// dashboard/js/render-managed.js — Live managed-processes panel + log viewer modal.
// P-6e2a8b13 (plan W-mp7k1r760003b5dd item 5).
//
// Polls /api/managed-processes with If-None-Match every refresh tick on the
// engine page (cheap: server returns 304 when nothing changed). Renders one
// project-grouped table per owner_project with name/pid/healthy/ports/attrs/
// uptime/TTL + per-row kill, restart, log buttons.
//
// Log button opens a self-contained modal overlay that streams the spec's
// log_path via SSE EventSource (`/api/managed-processes/log-stream/<name>`).
// One stream at a time — closing the modal aborts the EventSource so we
// don't blow through HTTP/1.1 connection slots (cf. live-stream.js note
// about why the agent live tab moved off SSE).

let _managedProcessesEtag = null;
// Cache the last successfully-fetched items so 304 (no-op) ticks can still
// re-render the time-derived columns (uptime, TTL) against the current
// Date.now(). The server payload only changes on spawn/kill/restart, so
// without this cache the strings would freeze between events
// (W-mpbt3wfs002l3a09).
let _managedProcessesLastItems = null;
let _managedLogES = null;

// Mount-point registry (W-mpd5ewhj000oc5c5 — QA tab dual-render).
// Each mount = { contentId, countId } pointing at a DOM container to populate.
// Both the Engine page and the QA page register their own mount, and
// renderManagedProcesses() writes to every mount that's currently in the DOM.
// The Engine page mount is registered eagerly here so the existing engine.html
// behavior keeps working without an explicit mount call.
const _managedMounts = [
  { contentId: 'managed-processes-content', countId: 'managed-processes-count' },
];

function mountManagedProcessesPanel(opts) {
  if (!opts || !opts.contentId) return;
  for (const m of _managedMounts) {
    if (m.contentId === opts.contentId) return; // already registered
  }
  _managedMounts.push({ contentId: opts.contentId, countId: opts.countId || null });
}

function unmountManagedProcessesPanel(contentId) {
  for (let i = _managedMounts.length - 1; i >= 0; i--) {
    if (_managedMounts[i].contentId === contentId) _managedMounts.splice(i, 1);
  }
}

// Read-only accessor for the last successfully-fetched managed-process items.
// Used by render-other.js renderKeepProcesses() to suppress PIDs that are
// already tracked as managed processes (W-mpdad3mq000m53bb dedup). Returns
// the live cache reference — callers must not mutate it.
function getLastItems() { return _managedProcessesLastItems || []; }

function _fmtAgo(ms) {
  if (!ms || ms < 0) return '0s';
  const s = Math.floor(ms / 1000);
  if (s < 60) return s + 's';
  const m = Math.floor(s / 60);
  if (m < 60) return m + 'm';
  const h = Math.floor(m / 60);
  if (h < 24) return h + 'h ' + (m % 60) + 'm';
  const d = Math.floor(h / 24);
  return d + 'd ' + (h % 24) + 'h';
}

function _fmtAttrs(attrs) {
  if (!attrs || typeof attrs !== 'object') return '';
  const parts = [];
  for (const k of Object.keys(attrs)) {
    const v = attrs[k];
    if (v == null) continue;
    const s = (typeof v === 'object') ? JSON.stringify(v) : String(v);
    // Compact: clip individual values, keep the panel scannable.
    parts.push(escHtml(k) + '=' + escHtml(s.length > 40 ? s.slice(0, 37) + '...' : s));
  }
  return parts.join(' ');
}

function _renderManagedTable(items) {
  const rows = items.map(function (s) {
    const healthBadge = s.healthy
      ? '<span style="color:var(--green)">●</span> healthy'
      : (s.alive ? '<span style="color:var(--yellow)">●</span> starting' : '<span style="color:var(--muted)">○</span> down');
    const ports = (s.ports && s.ports.length) ? s.ports.join(',') : '';
    const uptime = s.started_at ? _fmtAgo(Date.now() - s.started_at) : '';
    const ttl = s.ttl_expires_at ? _fmtAgo(s.ttl_expires_at - Date.now()) : '';
    const ttlColor = (s.ttl_expires_at && s.ttl_expires_at - Date.now() < 5 * 60 * 1000) ? 'var(--yellow)' : 'var(--muted)';
    const nameAttr = encodeURIComponent(s.name || '');
    return '<tr>' +
      '<td style="padding:4px 8px;font-family:monospace;font-size:var(--text-base)">' + escHtml(s.name || '') + '</td>' +
      '<td style="padding:4px 8px;font-family:monospace;font-size:var(--text-base)">' + (s.pid || '') + '</td>' +
      '<td style="padding:4px 8px;font-size:var(--text-base)">' + healthBadge + '</td>' +
      '<td style="padding:4px 8px;font-size:var(--text-base);font-family:monospace">' + escHtml(ports) + '</td>' +
      '<td style="padding:4px 8px;font-size:var(--text-sm);color:var(--muted);max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escHtml(_fmtAttrs(s.attrs).replace(/<[^>]+>/g, '')) + '">' + _fmtAttrs(s.attrs) + '</td>' +
      '<td style="padding:4px 8px;font-size:var(--text-base);color:var(--muted)">' + escHtml(uptime) + '</td>' +
      '<td style="padding:4px 8px;font-size:var(--text-base);color:' + ttlColor + '">' + escHtml(ttl) + '</td>' +
      '<td style="padding:4px 8px;text-align:right;white-space:nowrap">' +
        '<button onclick="openManagedLog(decodeURIComponent(\'' + nameAttr + '\'))" style="font-size:var(--text-sm);padding:2px 6px;margin-right:4px;border:1px solid var(--border);background:var(--surface);color:var(--fg);border-radius:3px;cursor:pointer">Log</button>' +
        '<button onclick="restartManagedSpec(decodeURIComponent(\'' + nameAttr + '\'))" style="font-size:var(--text-sm);padding:2px 6px;margin-right:4px;border:1px solid var(--blue);background:transparent;color:var(--blue);border-radius:3px;cursor:pointer">Restart</button>' +
        '<button onclick="killManagedSpec(decodeURIComponent(\'' + nameAttr + '\'))" style="font-size:var(--text-sm);padding:2px 6px;border:1px solid var(--red);background:transparent;color:var(--red);border-radius:3px;cursor:pointer">Kill</button>' +
      '</td>' +
      '</tr>';
  }).join('');
  return '<table style="width:100%;border-collapse:collapse;margin-bottom:8px">' +
    '<thead><tr style="text-align:left;font-size:var(--text-sm);color:var(--muted);text-transform:uppercase">' +
      '<th style="padding:4px 8px">name</th>' +
      '<th style="padding:4px 8px">pid</th>' +
      '<th style="padding:4px 8px">health</th>' +
      '<th style="padding:4px 8px">ports</th>' +
      '<th style="padding:4px 8px">attrs</th>' +
      '<th style="padding:4px 8px">uptime</th>' +
      '<th style="padding:4px 8px">ttl</th>' +
      '<th style="padding:4px 8px;text-align:right">actions</th>' +
    '</tr></thead><tbody>' + rows + '</tbody></table>';
}

async function renderManagedProcesses() {
  // Resolve every registered mount that's currently in the DOM (Engine page +
  // QA page when present). Skip the fetch entirely when nothing's mounted.
  const liveMounts = _managedMounts
    .map(m => ({ ...m, root: document.getElementById(m.contentId), countEl: m.countId ? document.getElementById(m.countId) : null }))
    .filter(m => m.root);
  if (!liveMounts.length) return;
  let items;
  let fetchErr = null;
  try {
    const headers = {};
    if (_managedProcessesEtag) headers['If-None-Match'] = _managedProcessesEtag;
    const res = await fetch('/api/managed-processes', { headers });
    if (res.status === 304) {
      // Server payload unchanged — reuse cached items so uptime/TTL columns
      // recompute against the current Date.now(). Defensive null check: a
      // first-tick 304 shouldn't happen (no prior ETag => no If-None-Match)
      // but if it does, skip this frame rather than wiping the panel.
      if (!_managedProcessesLastItems) return;
      items = _managedProcessesLastItems;
    } else {
      const et = res.headers.get('ETag');
      if (et) _managedProcessesEtag = et;
      const data = await res.json();
      items = (data && Array.isArray(data.items)) ? data.items : [];
      _managedProcessesLastItems = items;
    }
  } catch (e) {
    fetchErr = e;
  }
  let html;
  let countText;
  if (fetchErr) {
    countText = '?';
    html = '<span style="color:var(--red)">Failed to load: ' + escHtml(fetchErr.message) + '</span>';
  } else if (!items || !items.length) {
    countText = '0';
    html = '<p class="empty">No managed processes. Agents declare them via <code>agents/&lt;id&gt;/managed-spawn.json</code>.</p>';
  } else {
    countText = String(items.length);
    // Group by owner_project (empty/missing groups under "(unassigned)").
    const groups = {};
    for (const s of items) {
      const key = s.owner_project || '(unassigned)';
      (groups[key] = groups[key] || []).push(s);
    }
    const names = Object.keys(groups).sort();
    html = names.map(function (proj) {
      const group = groups[proj];
      group.sort(function (a, b) { return (a.name || '').localeCompare(b.name || ''); });
      return '<div style="border:1px solid var(--border);border-radius:4px;padding:8px;margin-bottom:8px;background:var(--surface2)">' +
        '<div style="font-weight:600;font-size:var(--text-base);margin-bottom:6px">' + escHtml(proj) +
          ' <span style="color:var(--muted);font-weight:400">(' + group.length + ')</span>' +
        '</div>' +
        _renderManagedTable(group) +
      '</div>';
    }).join('');
  }
  // DocumentFragment instead of innerHTML — keeps the file out of the
  // dynamic-innerHTML regression gate (cf. render-other.js renderKeepProcesses).
  // Build the fragment once per mount (DocumentFragment nodes get adopted on
  // first append so each mount needs its own copy).
  for (const m of liveMounts) {
    if (m.countEl) m.countEl.textContent = countText;
    // eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: fetch error, project, process name, ports, attrs, uptime, ttl)
    const frag = document.createRange().createContextualFragment(html);
    m.root.replaceChildren(frag);
  }
}

async function killManagedSpec(name) {
  if (!confirm('Kill managed process "' + name + '"? PID will be terminated and the spec removed from engine state.')) return;
  try {
    const res = await fetch('/api/managed-processes/kill', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name }),
    });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) {
      alert('Failed: ' + (data.error || res.status));
      return;
    }
    if (typeof showToast === 'function') showToast('cmd-toast', 'Killed ' + name + (data.killed_pid ? ' (PID ' + data.killed_pid + ')' : ''), true);
  } catch (e) {
    alert('Network error: ' + e.message);
  }
  _managedProcessesEtag = null; // force re-render
  renderManagedProcesses();
}

async function restartManagedSpec(name) {
  if (!confirm('Restart managed process "' + name + '"? The current PID will be killed and a new one spawned from saved state.')) return;
  try {
    const res = await fetch('/api/managed-processes/restart', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name }),
    });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) {
      alert('Failed: ' + (data.error || res.status));
      return;
    }
    if (typeof showToast === 'function') showToast('cmd-toast', 'Restarted ' + name + (data.pid ? ' (new PID ' + data.pid + ')' : ''), true);
  } catch (e) {
    alert('Network error: ' + e.message);
  }
  _managedProcessesEtag = null;
  renderManagedProcesses();
}

function _ensureManagedLogModal() {
  let overlay = document.getElementById('managed-log-modal');
  if (overlay) return overlay;
  overlay = document.createElement('div');
  overlay.id = 'managed-log-modal';
  overlay.style.cssText = 'display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:1000;align-items:center;justify-content:center;padding:24px';
  // Static markup, no user data — use createContextualFragment so we stay
  // out of the dynamic-innerHTML regression gate (test/unit.test.js DYNAMIC_INNERHTML_BASELINE).
  const tpl = '' +
    '<div style="background:var(--bg);border:1px solid var(--border);border-radius:6px;width:100%;max-width:1100px;height:80vh;display:flex;flex-direction:column">' +
      '<div style="display:flex;align-items:center;padding:8px 12px;border-bottom:1px solid var(--border)">' +
        '<div id="managed-log-title" style="flex:1;font-weight:600;font-size:var(--text-md);font-family:monospace">Managed log</div>' +
        '<div id="managed-log-status" style="font-size:var(--text-sm);color:var(--muted);margin-right:12px">connecting...</div>' +
        '<button id="managed-log-close-btn" style="font-size:var(--text-sm);padding:4px 10px;border:1px solid var(--border);background:transparent;color:var(--fg);border-radius:3px;cursor:pointer">Close</button>' +
      '</div>' +
      '<pre id="managed-log-body" style="flex:1;overflow:auto;margin:0;padding:8px 12px;font-family:monospace;font-size:var(--text-base);white-space:pre-wrap;word-break:break-all;background:var(--surface2)"></pre>' +
    '</div>';
  const frag = document.createRange().createContextualFragment(tpl);
  overlay.appendChild(frag);
  document.body.appendChild(overlay);
  overlay.addEventListener('click', function (e) {
    if (e.target === overlay) closeManagedLog();
  });
  const closeBtn = overlay.querySelector('#managed-log-close-btn');
  if (closeBtn) closeBtn.addEventListener('click', closeManagedLog);
  return overlay;
}

function openManagedLog(name) {
  closeManagedLog(); // single-stream invariant
  const overlay = _ensureManagedLogModal();
  const title = overlay.querySelector('#managed-log-title');
  const status = overlay.querySelector('#managed-log-status');
  const body = overlay.querySelector('#managed-log-body');
  if (title) title.textContent = 'Log: ' + name;
  if (body) body.textContent = '';
  if (status) status.textContent = 'connecting...';
  overlay.style.display = 'flex';

  try {
    const es = new EventSource('/api/managed-processes/log-stream/' + encodeURIComponent(name));
    _managedLogES = es;
    es.onopen = function () { if (status) status.textContent = 'streaming'; };
    es.onmessage = function (ev) {
      let chunk;
      try { chunk = JSON.parse(ev.data); }
      catch { chunk = ev.data; }
      if (typeof chunk !== 'string') chunk = String(chunk);
      if (!body) return;
      const wasAtBottom = (body.scrollHeight - body.scrollTop - body.clientHeight) < 40;
      body.appendChild(document.createTextNode(chunk));
      if (wasAtBottom) body.scrollTop = body.scrollHeight;
    };
    es.addEventListener('done', function (ev) {
      if (status) status.textContent = 'closed (' + (ev.data || 'done') + ')';
      try { es.close(); } catch { /* optional */ }
      if (_managedLogES === es) _managedLogES = null;
    });
    es.onerror = function () {
      if (status) status.textContent = 'disconnected';
      // EventSource auto-reconnects; if the spec is gone the server will emit
      // a 'done' event on the next attempt.
    };
  } catch (e) {
    if (status) status.textContent = 'failed: ' + e.message;
  }
}

function closeManagedLog() {
  if (_managedLogES) {
    try { _managedLogES.close(); } catch { /* optional */ }
    _managedLogES = null;
  }
  const overlay = document.getElementById('managed-log-modal');
  if (overlay) overlay.style.display = 'none';
}

window.MinionsManagedProcesses = {
  renderManagedProcesses,
  killManagedSpec,
  restartManagedSpec,
  openManagedLog,
  closeManagedLog,
  mountManagedProcessesPanel,
  unmountManagedProcessesPanel,
  getLastItems,
};
