// render-meetings.js — Team meeting rendering

let _showArchived = false;
const MTG_PER_PAGE = 25;
let _mtgPage = 0;
let _lastMeetingHash = '';
let _lastMeetingsForPaging = [];
let _mtgTotalPages = 1;

// Pull meetings straight off disk via /state/meetings (dir listing) +
// per-file fetch of /state/meetings/<id>.json. Each file is slimmed
// client-side to match dashboard.js _slimMeetingForStatus so the
// renderer sees the same shape it always has (findings/debate reduced
// to {agentId: true} sentinels, transcript/conclusion bodies dropped).
// Detail view still calls /api/meetings/<id> for the full bodies.
// Issue #2949 — meetings on /api/status used to inherit the same outer
// cache staleness as work-items; this disk-direct path sidesteps it.
function _slimMeetingForRender(m) {
  if (!m || typeof m !== 'object') return null;
  const findingsKeys = m.findings && typeof m.findings === 'object' ? Object.keys(m.findings) : [];
  const debateKeys = m.debate && typeof m.debate === 'object' ? Object.keys(m.debate) : [];
  const findings = {};
  for (const k of findingsKeys) findings[k] = true;
  const debate = {};
  for (const k of debateKeys) debate[k] = true;
  const slim = {
    id: m.id,
    title: m.title,
    status: m.status,
    round: m.round,
    participants: Array.isArray(m.participants) ? m.participants : [],
    agenda: m.agenda,
    createdAt: m.createdAt,
    findings,
    debate,
  };
  if (m.completedAt !== undefined) slim.completedAt = m.completedAt;
  if (m.roundStartedAt !== undefined) slim.roundStartedAt = m.roundStartedAt;
  if (m.createdBy !== undefined) slim.createdBy = m.createdBy;
  return slim;
}
async function fetchMeetingsFromDisk() {
  try {
    const listResp = await fetch('/state/meetings');
    if (!listResp.ok) return null;
    const listing = await listResp.json();
    const entries = Array.isArray(listing && listing.entries) ? listing.entries : [];
    const jsonFiles = entries.filter(e => e && !e.isDir && typeof e.name === 'string' && e.name.endsWith('.json'));
    const meetings = await Promise.all(jsonFiles.map(async (e) => {
      try {
        const r = await fetch('/state/meetings/' + encodeURIComponent(e.name));
        if (!r.ok) return null;
        const raw = await r.json();
        return _slimMeetingForRender(raw);
      } catch {
        return null;
      }
    }));
    return meetings.filter(Boolean);
  } catch {
    return null;
  }
}

function renderMeetings(meetings, opts) {
  opts = opts || {};
  meetings = (meetings || []).filter(function(m) { return !isDeleted('mtg:' + m.id); });
  meetings.sort((a, b) => (b.createdAt || b.completedAt || '').localeCompare(a.createdAt || a.completedAt || ''));
  _lastMeetingsForPaging = meetings;
  const el = document.getElementById('meetings-content');
  const countEl = document.getElementById('meetings-count');
  const scrollState = opts.preserveScroll === false ? null : captureDashboardScrollState(el, [':self']);
  if (!meetings || meetings.length === 0) {
    countEl.textContent = '0';
    el.innerHTML = '<p class="empty">No meetings yet. Start one to have agents investigate, debate, and conclude on a topic.</p>';
    _mtgTotalPages = 1;
    restoreDashboardScrollState(el, scrollState);
    return;
  }

  const active = meetings.filter(m => m.status !== 'archived');
  const archived = meetings.filter(m => m.status === 'archived');
  countEl.textContent = active.length;

  const statusColors = { investigating: 'var(--blue)', debating: 'var(--purple,#a855f7)', concluding: 'var(--yellow)', completed: 'var(--green)', archived: 'var(--muted)' };
  const statusLabels = { investigating: 'Round 1 — Investigating', debating: 'Round 2 — Debating', concluding: 'Round 3 — Concluding', completed: 'Completed', archived: 'Archived' };

  const visible = _showArchived ? meetings : active;
  if (visible.length === 0) {
    el.innerHTML = '<p class="empty">No active meetings.</p>';
    // eslint-disable-next-line no-unsanitized/method -- reason: composed from internal archived count and fixed toggle markup (no user data flows in)
    if (archived.length) el.insertAdjacentHTML('beforeend', '<div style="text-align:center;margin-top:8px"><button class="pr-pager-btn" style="font-size:var(--text-sm)" onclick="_toggleArchivedMeetings()">Show ' + archived.length + ' archived</button></div>');
    _mtgTotalPages = 1;
    restoreDashboardScrollState(el, scrollState);
    return;
  }

  const totalPages = Math.ceil(visible.length / MTG_PER_PAGE);
  _mtgTotalPages = totalPages;
  if (_mtgPage >= totalPages) _mtgPage = totalPages - 1;
  const start = _mtgPage * MTG_PER_PAGE;
  const pageItems = visible.slice(start, start + MTG_PER_PAGE);

  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: meeting id, title, participants, time, agenda)
  el.innerHTML = pageItems.map(m => {
    const statusColor = statusColors[m.status] || 'var(--muted)';
    const statusLabel = statusLabels[m.status] || m.status;
    const participantBadges = (m.participants || []).map(p => {
      const hasFindings = m.findings?.[p];
      const hasDebate = m.debate?.[p];
      const icon = m.status === 'completed' ? '✓' : m.status === 'debating' ? (hasDebate ? '✓' : (hasFindings ? '⏳' : '○')) : (hasFindings ? '✓' : '⏳');
      return '<span style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-size:var(--text-sm)">' + icon + ' ' + escHtml(p) + '</span>';
    }).join(' ');

    const dt = m.completedAt || m.createdAt;
    const timeStr = dt ? formatLocalDateTime(dt) : '';

    return '<div data-file="meetings/' + escHtml(m.id) + '.json" style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:8px;cursor:pointer;position:relative" onclick="if(shouldIgnoreSelectionClick(event))return;openMeetingDetail(\'' + escHtml(m.id) + '\')">' +
      '<div style="display:flex;justify-content:space-between;align-items:center">' +
        '<strong style="font-size:var(--text-lg)">' + escHtml(m.title) + '</strong>' +
        '<div style="display:flex;align-items:center;gap:8px">' +
          '<span style="color:' + statusColor + ';font-size:var(--text-base);font-weight:600">' + statusLabel + '</span>' +
          (m.status === 'archived'
            ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px" onclick="event.stopPropagation();_unarchiveMeeting(\'' + escHtml(m.id) + '\')">Unarchive</button>'
            : (m.status === 'completed' ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px" onclick="event.stopPropagation();_archiveMeeting(\'' + escHtml(m.id) + '\')">Archive</button>' : '')) +
          '<button class="btn-destructive" onclick="event.stopPropagation();_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
        '</div>' +
      '</div>' +
      (timeStr ? '<div style="margin-top:4px;font-size:var(--text-sm);color:var(--muted)">' + escHtml(timeStr) + '</div>' : '') +
      '<div style="margin-top:6px;display:flex;gap:6px;flex-wrap:wrap">' + participantBadges + '</div>' +
      '<div style="margin-top:6px;font-size:var(--text-base);color:var(--muted)">' + escHtml((m.agenda || '').slice(0, 100)) + (m.agenda?.length > 100 ? '...' : '') + '</div>' +
    '</div>';
  }).join('');

  if (visible.length > MTG_PER_PAGE) {
    // eslint-disable-next-line no-unsanitized/method -- reason: composed from internal numeric page bounds and fixed pager actions (no user data flows in)
    el.insertAdjacentHTML('beforeend', '<div class="pr-pager">' +
      '<span class="pr-page-info">Showing ' + (start + 1) + ' to ' + Math.min(start + MTG_PER_PAGE, visible.length) + ' of ' + visible.length + '</span>' +
      '<div class="pr-pager-btns">' +
        '<button class="pr-pager-btn ' + (_mtgPage === 0 ? 'disabled' : '') + '" onclick="_mtgPrev()">Prev</button>' +
        '<button class="pr-pager-btn ' + (_mtgPage >= totalPages - 1 ? 'disabled' : '') + '" onclick="_mtgNext()">Next</button>' +
      '</div></div>');
  }

  if (archived.length > 0) {
    // eslint-disable-next-line no-unsanitized/method -- reason: composed from internal archived count and fixed toggle label (no user data flows in)
    el.insertAdjacentHTML('beforeend', '<div style="text-align:center;margin-top:8px"><button class="pr-pager-btn" style="font-size:var(--text-sm)" onclick="_toggleArchivedMeetings()">' +
      (_showArchived ? 'Hide' : 'Show') + ' ' + archived.length + ' archived</button></div>');
  }
  restoreDashboardScrollState(el, scrollState);
  restoreNotifBadges();
}

function _rerenderMeetingPageFromCache(opts) {
  renderMeetings(_lastMeetingsForPaging || [], opts);
}
function _mtgPrev() {
  if (_mtgPage > 0) {
    _mtgPage--;
    _rerenderMeetingPageFromCache({ preserveScroll: false });
  }
}
function _mtgNext() {
  if (_mtgPage < _mtgTotalPages - 1) {
    _mtgPage++;
    _rerenderMeetingPageFromCache({ preserveScroll: false });
  }
}

function _toggleArchivedMeetings() {
  _showArchived = !_showArchived;
  _mtgPage = 0;
  _rerenderMeetingPageFromCache({ preserveScroll: false });
}

let _meetingPollInterval = null;
let _meetingPollId = null;

function _stopMeetingPoll() {
  if (_meetingPollInterval) { clearInterval(_meetingPollInterval); _meetingPollInterval = null; }
  _meetingPollId = null;
  // F2 (W-mpgb0xe6000gb66d): drop linked-plan recheck cache when the modal closes.
  _modalMeetingForLinkedPlanRecheck = null;
  _modalLastLinkedPlanFile = null;
}

function _renderMeetingDetail(m) {
      const statusColors = { investigating: 'var(--blue)', debating: 'var(--purple,#a855f7)', concluding: 'var(--yellow)', completed: 'var(--green)' };
      const statusLabels = { investigating: 'Round 1 — Investigating', debating: 'Round 2 — Debating', concluding: 'Round 3 — Concluding', completed: 'Completed' };

      let html = '<div style="display:flex;flex-direction:column;gap:12px">';

      // Status bar
      html += '<div style="display:flex;justify-content:space-between;align-items:center">' +
        '<span style="color:' + (statusColors[m.status] || 'var(--muted)') + ';font-weight:600">' + (statusLabels[m.status] || m.status) + '</span>' +
        '<span style="font-size:var(--text-sm);color:var(--muted)">' + escHtml(m.createdAt?.slice(0, 16).replace('T', ' ') || '') + '</span>' +
      '</div>';

      // Agenda — render markdown-like formatting
      html += '<div style="background:var(--surface2);padding:8px 12px;border-radius:6px;font-size:var(--text-md);white-space:pre-wrap;line-height:1.6">' +
        '<strong>Agenda:</strong>\n' + renderMd(m.agenda || '') + '</div>';

      // Per-agent panels
      for (const agent of (m.participants || [])) {
        html += '<div style="border:1px solid var(--border);border-radius:6px;overflow:hidden">';
        html += '<div style="background:var(--surface2);padding:6px 12px;font-weight:600;font-size:var(--text-md)">' + escHtml(agent) + '</div>';

        // Findings
        if (m.findings?.[agent]) {
          html += '<div style="padding:8px 12px;font-size:var(--text-base);border-bottom:1px solid var(--border)">' +
            '<div style="color:var(--muted);font-size:var(--text-sm);margin-bottom:4px">Round 1 — Findings</div>' +
            '<div class="meeting-scroll-panel" style="word-break:break-word;max-height:300px;overflow-y:auto">' + renderMd(m.findings[agent].content || '') + '</div></div>';
        }

        // Debate
        if (m.debate?.[agent]) {
          html += '<div style="padding:8px 12px;font-size:var(--text-base);border-bottom:1px solid var(--border)">' +
            '<div style="color:var(--muted);font-size:var(--text-sm);margin-bottom:4px">Round 2 — Debate</div>' +
            '<div class="meeting-scroll-panel" style="word-break:break-word;max-height:300px;overflow-y:auto">' + renderMd(m.debate[agent].content || '') + '</div></div>';
        }

        // Status
        if (!m.findings?.[agent] && m.status !== 'completed') {
          html += '<div style="padding:6px 12px;font-size:var(--text-sm);color:var(--muted)">⏳ Waiting...</div>';
        }

        html += '</div>';
      }

      // Conclusion
      if (m.conclusion) {
        html += '<div style="background:rgba(63,185,80,0.08);border:1px solid var(--green);border-radius:6px;padding:10px 14px">' +
          '<div style="color:var(--green);font-weight:600;font-size:var(--text-md);margin-bottom:6px">Conclusion (by ' + escHtml(m.conclusion.agent || '?') + ')</div>' +
          '<div class="meeting-scroll-panel" style="font-size:var(--text-md);word-break:break-word;max-height:400px;overflow-y:auto">' + renderMd(m.conclusion.content || '') + '</div></div>';
      }

      // Human notes
      if (m.humanNotes?.length > 0) {
        html += '<div style="border-top:1px solid var(--border);padding-top:8px">' +
          '<div style="color:var(--muted);font-size:var(--text-sm);margin-bottom:4px">Human Notes</div>' +
          m.humanNotes.map(n => '<div style="font-size:var(--text-base);margin-bottom:2px">• ' + escHtml(n) + '</div>').join('') +
        '</div>';
      }

      // Actions
      if (m.status === 'archived') {
        html += '<div style="display:flex;gap:8px;border-top:1px solid var(--border);padding-top:8px">' +
          '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px" onclick="_unarchiveMeeting(\'' + escHtml(m.id) + '\')">Unarchive</button>' +
          '<button class="btn-destructive btn-destructive-md" onclick="_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
        '</div>';
      } else if (m.status === 'completed') {
        const linkedPlan = _findLinkedPlan(m);
        html += '<div style="display:flex;gap:8px;flex-wrap:wrap;border-top:1px solid var(--border);padding-top:8px">' +
          (linkedPlan
            ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--blue);border-color:var(--blue)" onclick="_viewPlanWithBack(\'' + escHtml(linkedPlan.file) + '\',\'' + escHtml(m.id) + '\')">View Plan</button>' +
              '<button class="btn-action btn-action-md" onclick="_createPlanFromMeeting(\'' + escHtml(m.id) + '\',this)">New Plan</button>'
            : '<button class="btn-action btn-action-md" onclick="_createPlanFromMeeting(\'' + escHtml(m.id) + '\',this)">Create Plan from Meeting</button>') +
          '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px" onclick="_archiveMeeting(\'' + escHtml(m.id) + '\')">Archive</button>' +
          '<button class="btn-destructive btn-destructive-md" onclick="_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
        '</div>' +
        '<div id="meeting-plan-toast" class="cmd-toast cmd-toast-inline" style="margin-top:6px"></div>' +
        '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:4px">Use the Q&amp;A below to discuss action items' + (linkedPlan ? '' : ', then create a plan to execute them') + '.</div>';
      } else {
        html += '<div style="display:flex;gap:8px;border-top:1px solid var(--border);padding-top:8px">' +
          '<input id="meeting-note-input" type="text" placeholder="Add context for all agents..." style="flex:1;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md)" onkeydown="if(event.key===\'Enter\')_submitMeetingNote(\'' + escHtml(m.id) + '\')">' +
          '<button onclick="_submitMeetingNote(\'' + escHtml(m.id) + '\',this)" style="padding:6px 12px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-size:var(--text-base)">Add Note</button>' +
        '</div>' +
        '<div style="display:flex;gap:8px;margin-top:4px">' +
          '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--yellow);border-color:var(--yellow)" onclick="_advanceMeeting(\'' + escHtml(m.id) + '\',this)">Skip to Next Round</button>' +
          '<button class="btn-destructive btn-destructive-md" onclick="_endMeeting(\'' + escHtml(m.id) + '\',this)">End Meeting</button>' +
          '<button class="btn-destructive btn-destructive-md" onclick="_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
        '</div>';
      }

      html += '</div>';

      document.getElementById('modal-title').textContent = 'Meeting: ' + m.title;
      var body = document.getElementById('modal-body');
      var scrollState = captureDashboardScrollState(body, [':self', '.meeting-scroll-panel']);
      // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled markdown fields before assembling HTML (see dashboard/js/utils.js); ids and labels use escHtml()
      body.innerHTML = html;
      body.style.fontFamily = "'Segoe UI', system-ui, sans-serif";
      body.style.whiteSpace = 'normal';
      restoreDashboardScrollState(body, scrollState);

      // Wire up doc-chat Q&A panel for the meeting transcript
      const transcript = (m.transcript || []).map(t =>
        '### ' + (t.agent || 'agent') + ' (' + (t.type || '') + ', Round ' + (t.round || '?') + ')\n\n' + (t.content || '')
      ).join('\n\n---\n\n');
      const meetingDoc = '# Meeting: ' + m.title + '\n\n**Agenda:** ' + m.agenda + '\n\n' + transcript;
      _modalDocContext = { title: 'Meeting: ' + m.title, content: meetingDoc, selection: '' };
      // Always set filePath so doc-chat detects this as a meeting (uses Sonnet with tools).
      // Server-side handleDocChat prevents writes to completed meeting JSON.
      _modalFilePath = 'meetings/' + m.id + '.json';
      try { showModalQa(); } catch { /* expected if QA not loaded */ }

      document.getElementById('modal').classList.add('open');

      // F2 (W-mpgb0xe6000gb66d): refresh the linked-plan cache after each
      // render so refreshPlans() can detect a flip and re-render the modal.
      _setModalMeetingForLinkedPlanRecheck(m);
}

function openMeetingDetail(id) {
  _stopMeetingPoll();
  fetch('/api/meetings/' + encodeURIComponent(id))
    .then(r => r.json())
    .then(data => {
      if (!data.meeting) { alert('Meeting not found'); return; }
      _lastMeetingHash = JSON.stringify(data.meeting);
      _renderMeetingDetail(data.meeting);

      // Live-poll while modal is open
      _meetingPollId = id;
      _meetingPollInterval = setInterval(function() {
        if (!document.getElementById('modal')?.classList?.contains('open') || _meetingPollId !== id) {
          _stopMeetingPoll(); return;
        }
        fetch('/api/meetings/' + encodeURIComponent(id))
          .then(r => r.json())
          .then(d => {
            if (d.meeting && _meetingPollId === id) {
              const hash = JSON.stringify(d.meeting);
              if (hash === _lastMeetingHash) return; // no change — skip re-render
              _lastMeetingHash = hash;
              _renderMeetingDetail(d.meeting);
            }
          })
          .catch(function() {});
      }, 3000);
    })
    .catch(e => alert('Error: ' + e.message));
}

function openCreateMeetingModal() {
  const agentOpts = (typeof cmdAgents !== 'undefined' ? cmdAgents : []).map(a =>
    '<label style="display:flex;align-items:center;gap:6px;cursor:pointer"><input type="checkbox" value="' + escHtml(a.id) + '" checked style="accent-color:var(--blue)"> ' + escHtml(a.name) + ' (' + escHtml(a.role || '') + ')</label>'
  ).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';

  document.getElementById('modal-title').textContent = 'New Team Meeting';
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: agent id, name, role)
  document.getElementById('modal-body').innerHTML =
    '<div style="display:flex;flex-direction:column;gap:10px">' +
      '<label style="color:var(--text);font-size:var(--text-md)">Title<input id="mtg-title" style="' + inputStyle + '" placeholder="e.g. Should we add SQLite?"></label>' +
      '<label style="color:var(--text);font-size:var(--text-md)">Agenda<textarea id="mtg-agenda" rows="4" style="' + inputStyle + ';resize:vertical" placeholder="What should agents investigate and debate? Be specific about the question to resolve."></textarea></label>' +
      '<div style="color:var(--text);font-size:var(--text-md)">Participants<div style="display:flex;flex-direction:column;gap:4px;margin-top:4px" id="mtg-participants">' + agentOpts + '</div></div>' +
      '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
        '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
        '<button onclick="_submitCreateMeeting(event)" class="btn-primary-lg">Start Meeting</button>' +
      '</div>' +
    '</div>';
  document.getElementById('modal').classList.add('open');
}

async function _submitCreateMeeting(e) {
  var btn = (e || window.event)?.target; if (btn) { btn.disabled = true; btn.textContent = 'Starting...'; }
  const title = document.getElementById('mtg-title')?.value?.trim();
  const agenda = document.getElementById('mtg-agenda')?.value?.trim();
  if (!title || !agenda) { if (btn) { btn.disabled = false; btn.textContent = 'Start Meeting'; } alert('Title and agenda required'); return; }
  const checks = document.querySelectorAll('#mtg-participants input[type="checkbox"]:checked');
  const participants = [...checks].map(c => c.value);
  if (participants.length < 2) { if (btn) { btn.disabled = false; btn.textContent = 'Start Meeting'; } alert('Select at least 2 participants'); return; }
  try { closeModal(); } catch { /* expected */ }
  showToast('cmd-toast', 'Meeting started with ' + participants.length + ' agents', true);
  try {
    const res = await fetch('/api/meetings', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title, agenda, participants })
    });
    const data = await res.json();
    if (res.ok) { wakeEngine(); refresh(); }
    else { showToast('cmd-toast', 'Failed: ' + (data.error || 'unknown'), false); openCreateMeetingModal(); }
  } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); openCreateMeetingModal(); }
}

async function _submitMeetingNote(id, btn) {
  const input = document.getElementById('meeting-note-input');
  if (!input?.value?.trim()) return;
  const note = input.value.trim();
  input.value = '';
  if (btn) { btn.textContent = 'Adding...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
  showToast('cmd-toast', 'Note added', true);
  try {
    const res = await fetch('/api/meetings/note', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id, note })
    });
    if (res.ok) { /* already toasted */ }
    else { input.value = note; showToast('cmd-toast', 'Failed to add note', false); }
  } catch (e) { input.value = note; showToast('cmd-toast', 'Error: ' + e.message, false); }
  if (btn) { btn.textContent = 'Add Note'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
}

async function _advanceMeeting(id, btn) {
  if (!confirm('Skip to next round? Agents that haven\'t finished will be skipped.')) return;
  if (btn) { btn.textContent = 'Advancing...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
  showToast('cmd-toast', 'Advanced to next round', true);
  try {
    const res = await fetch('/api/meetings/advance', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id })
    });
    if (res.ok) {
      wakeEngine();
    } else {
      const d = await res.json().catch(function() { return {}; });
      showToast('cmd-toast', 'Advance failed: ' + (d.error || 'unknown'), false);
    }
  } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
  if (btn) { btn.textContent = 'Skip to Next Round'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
}

async function _endMeeting(id, btn) {
  if (!confirm('End this meeting? Current round will be stopped.')) return;
  if (btn) { btn.textContent = 'Ending...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
  showToast('cmd-toast', 'Meeting ended', true);
  try {
    const res = await fetch('/api/meetings/end', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id })
    });
    if (res.ok) { /* already toasted */ }
    else {
      const d = await res.json().catch(function() { return {}; });
      showToast('cmd-toast', 'End failed: ' + (d.error || 'unknown'), false);
      if (btn) { btn.textContent = 'End Meeting'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
    }
  } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); if (btn) { btn.textContent = 'End Meeting'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } }
}

async function _archiveMeeting(id) {
  showToast('cmd-toast', 'Meeting archived', true);
  markDeleted('mtg:' + id);
  try { closeModal(); } catch { /* may not be open */ }
  document.querySelectorAll('[onclick*="openMeetingDetail(\'' + id + '\')"]').forEach(function(el) { el.remove(); });
  try {
    const res = await fetch('/api/meetings/archive', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id })
    });
    if (!res.ok) { clearDeleted('mtg:' + id); const d = await res.json().catch(() => ({})); showToast('cmd-toast', 'Failed: ' + (d.error || 'unknown'), false); refresh(); }
  } catch (e) { clearDeleted('mtg:' + id); showToast('cmd-toast', 'Error: ' + e.message, false); refresh(); }
}

async function _unarchiveMeeting(id) {
  try { closeModal(); } catch { /* may not be open */ }
  document.querySelectorAll('[onclick*="openMeetingDetail(\'' + id + '\')"]').forEach(function(el) { el.remove(); });
  showToast('cmd-toast', 'Meeting unarchived', true);
  try {
    const res = await fetch('/api/meetings/unarchive', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id })
    });
    if (!res.ok) { const d = await res.json().catch(() => ({})); showToast('cmd-toast', 'Unarchive failed: ' + (d.error || 'unknown'), false); refresh(); }
  } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); refresh(); }
}

function _viewPlanWithBack(file, meetingId) {
  _stopMeetingPoll();
  planView(file);
  // After modal opens, prepend a back button to return to meeting
  setTimeout(function() {
    const title = document.getElementById('modal-title');
    if (title && !title.querySelector('.mtg-back-btn')) {
      const back = document.createElement('button');
      back.className = 'pr-pager-btn mtg-back-btn';
      back.style.cssText = 'font-size:var(--text-xs);padding:2px 8px;margin-right:8px;vertical-align:middle';
      back.textContent = '\u2190 Back to Meeting';
      back.onclick = function() { openMeetingDetail(meetingId); };
      title.prepend(back);
    }
  }, 100);
}

function _findLinkedPlan(meeting) {
  // F2 (W-mpgb0xe6000gb66d): prefer window._lastPlans (refreshed by refreshPlans
  // every ~12s) over window._lastStatus.plans, which is gated on the next
  // /api/status fetch and can lag a newly-created plan by an entire refresh
  // cycle. Falling back to _lastStatus.plans preserves first-paint behavior
  // before refreshPlans has run.
  var plans = (Array.isArray(window._lastPlans) ? window._lastPlans : null)
    || window._lastStatus?.plans
    || [];
  // Check 1: regex in conclusion text (agent may reference plan path)
  if (meeting?.conclusion?.content) {
    var match = meeting.conclusion.content.match(/plans\/([\w-]+\.md)/);
    if (match) {
      var file = match[1];
      return plans.find(function(p) { return p.file === file; }) || { file, summary: file };
    }
  }
  // Check 2: title-slug filename match (dashboard naming convention: "meeting-follow-up-{title}")
  if (meeting?.title) {
    var titleSlug = ('Meeting follow-up: ' + meeting.title).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50);
    var found = plans.find(function(p) { return p.file && p.file.startsWith(titleSlug); });
    if (found) return found;
  }
  return null;
}

// F2 (W-mpgb0xe6000gb66d): post-refreshPlans hook. Cache the meeting
// currently displayed in the modal so refreshPlans() can re-evaluate
// _findLinkedPlan and re-render the action row when a freshly-created plan
// now matches. Without this, the modal poll loop (3s) only re-renders on
// meeting hash changes, so a plan created in the background never flips
// "Create Plan from Meeting" → "View Plan".
var _modalMeetingForLinkedPlanRecheck = null;
var _modalLastLinkedPlanFile = null;

function _setModalMeetingForLinkedPlanRecheck(meeting) {
  if (meeting && meeting.status === 'completed') {
    _modalMeetingForLinkedPlanRecheck = meeting;
    _modalLastLinkedPlanFile = _findLinkedPlan(meeting)?.file || null;
  } else {
    _modalMeetingForLinkedPlanRecheck = null;
    _modalLastLinkedPlanFile = null;
  }
}

function _rerenderMeetingLinkedPlanIfChanged() {
  if (!_modalMeetingForLinkedPlanRecheck) return;
  var modalOpen = document.getElementById('modal')?.classList?.contains('open');
  if (!modalOpen || !_meetingPollId) return;
  var nextFile = _findLinkedPlan(_modalMeetingForLinkedPlanRecheck)?.file || null;
  if (nextFile === _modalLastLinkedPlanFile) return;
  // Linked-plan resolution changed — re-render the modal (preserves scroll;
  // _renderMeetingDetail will refresh _modalLastLinkedPlanFile via the cache hook).
  _renderMeetingDetail(_modalMeetingForLinkedPlanRecheck);
}

async function _createPlanFromMeeting(id, btn) {
  // Optimistic: instant feedback before fetching the meeting + posting the WI.
  showToast('meeting-plan-toast', 'Plan task queued — agent will draft the plan from this meeting', true);
  try {
    const res = await fetch('/api/meetings/' + encodeURIComponent(id));
    const data = await res.json();
    if (!data.meeting) { showToast('meeting-plan-toast', 'Meeting not found', false); return; }
    const m = data.meeting;

    const transcript = (m.transcript || []).map(function(t) {
      return '### ' + (t.agent || 'agent') + ' (' + (t.type || '') + ', Round ' + (t.round || '?') + ')\n\n' + (t.content || '');
    }).join('\n\n---\n\n');

    let humanContext = '';
    const qaThread = document.getElementById('modal-qa-thread');
    if (qaThread) {
      const qaText = qaThread.innerText.trim();
      if (qaText.length > 20) humanContext = '\n\n## Human Discussion\n\n' + qaText.slice(0, 3000);
    }

    const description =
      'Create an actionable implementation plan from the meeting below. Extract concrete action items from the conclusion and debates. ' +
      'For each item include: what to do, which files/areas to change, priority (high/medium/low), and estimated complexity (small/medium/large). ' +
      'Structure it as a plan ready for execution.\n\n' +
      '## Meeting: ' + (m.title || id) + '\n\n' +
      '**Agenda:** ' + (m.agenda || '') + '\n\n' +
      transcript + humanContext +
      '\n\n**Source Meeting ID:** ' + id;

    const title = 'Meeting follow-up: ' + (m.title || id);
    const planRes = await fetch('/api/plan', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title, description, priority: 'high' })
    });
    const planData = await planRes.json();
    if (!planRes.ok || !planData.ok) {
      showToast('meeting-plan-toast', 'Failed to queue plan task: ' + (planData.error || 'unknown'), false);
      return;
    }
    if (planData.id) {
      showToast('meeting-plan-toast', 'Plan task queued (' + planData.id + ')', true);
    }
    if (typeof wakeEngine === 'function') wakeEngine();
    if (typeof refresh === 'function') refresh();
  } catch (e) {
    showToast('meeting-plan-toast', 'Error: ' + e.message, false);
  }
}

async function _deleteMeeting(id) {
  if (!confirm('Delete this meeting? This cannot be undone.')) return;
  showToast('cmd-toast', 'Meeting deleted', true);
  markDeleted('mtg:' + id);
  try { closeModal(); } catch { /* may not be open */ }
  document.querySelectorAll('[onclick*="openMeetingDetail(\'' + id + '\')"]').forEach(function(el) { el.remove(); });
  try {
    const res = await fetch('/api/meetings/delete', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id })
    });
    if (!res.ok) { clearDeleted('mtg:' + id); const d = await res.json().catch(() => ({})); showToast('cmd-toast', 'Failed: ' + (d.error || 'unknown'), false); refresh(); }
  } catch (e) { clearDeleted('mtg:' + id); showToast('cmd-toast', 'Error: ' + e.message, false); refresh(); }
}

window.MinionsMeetings = { renderMeetings, openMeetingDetail, openCreateMeetingModal, _rerenderMeetingLinkedPlanIfChanged };
