// dashboard/js/render-utils.js — Shared formatting helpers for agent output rendering
// Depends on: escHtml() and renderMd() from utils.js (loaded before this file)

// marker + color per tool-call status. ACP `tool_call_update` flips chips from
// pending → completed/failed mid-stream so the user sees real progress; the
// non-pool (Claude direct) path emits status=null and stays on the neutral dot.
var _CC_TOOL_CHIP_STYLE = {
  pending:   { marker: '&#9679;', color: 'var(--muted)' },
  completed: { marker: '&#10003;', color: 'var(--green, #4ade80)' },
  failed:    { marker: '&#10005;', color: 'var(--red, #ef4444)' },
};

function renderToolChip(t) {
  var name = typeof t === 'string' ? t : t.name;
  var input = typeof t === 'string' ? {} : (t.input || {});
  var status = typeof t === 'string' ? null : (t.status || null);
  var style = _CC_TOOL_CHIP_STYLE[status] || _CC_TOOL_CHIP_STYLE.pending;
  return '<div style="color:' + style.color + ';font-size:var(--text-sm);font-family:monospace">'
    + '<span style="flex-shrink:0">' + style.marker + '</span> '
    + formatToolSummary(name, input)
    + '</div>';
}

/**
 * Returns a one-line human-readable summary for a Claude tool call.
 * @param {string} name - Tool name (e.g. 'Bash', 'Read', 'Edit')
 * @param {object} input - Tool input object
 * @returns {string} Human-readable summary (HTML-escaped)
 */
function formatToolSummary(name, input) {
  var inp = input || {};
  switch (name) {
    case 'Bash': {
      var cmd = String(inp.command || '');
      if (cmd.length > 80) cmd = cmd.slice(0, 77) + '...';
      return '$ ' + escHtml(cmd);
    }
    case 'Read':
      return 'Reading ' + escHtml(inp.file_path || '');
    case 'Edit':
      return 'Editing ' + escHtml(inp.file_path || '');
    case 'Write':
      return 'Writing ' + escHtml(inp.file_path || '');
    case 'Grep': {
      var pat = escHtml(inp.pattern || '');
      var gPath = escHtml(inp.path || '.');
      return 'Searching `' + pat + '` in ' + gPath;
    }
    case 'Glob':
      return 'Glob ' + escHtml(inp.pattern || '');
    case 'Agent':
      return 'Spawning agent: ' + escHtml(inp.description || '');
    case 'WebFetch':
      return 'Fetch ' + escHtml(inp.url || '');
    case 'WebSearch':
      return 'Search "' + escHtml(inp.query || '') + '"';
    case 'TodoWrite': {
      var items = Array.isArray(inp.todos) ? inp.todos : [];
      return 'Update todos (' + items.length + ' items)';
    }
    default: {
      var keys = Object.keys(inp);
      // No-input case (e.g. ACP chips that carry their full label in `name`
      // like "Fetch /api/status") — render the name verbatim, no parens.
      if (keys.length === 0) return escHtml(name);
      var firstKey = keys[0];
      var firstVal = String(inp[firstKey] || '');
      if (firstVal.length > 40) firstVal = firstVal.slice(0, 37) + '...';
      return escHtml(name) + '(' + escHtml(firstKey) + ': ' + escHtml(firstVal) + ')';
    }
  }
}

/**
 * Internal helper: renders a single parsed JSON object into an HTML fragment.
 * @param {object} obj - Parsed JSON object from agent JSONL output
 * @param {object} [state] - Mutable render state shared across calls in a single
 *   renderAgentOutput pass. Persistent fields: copilotToolKeys (Set), copilotDeltaBuffer,
 *   copilotReasoningPending. Per-line fields written by the caller before each call:
 *   isFinalResult (true only for the LAST result line before [process-exit]; gates the
 *   completion banner so ScheduleWakeup resume cycles don't repeatedly fire it),
 *   exitInfo ({ code, success } parsed from [process-exit], or null).
 * @returns {string} HTML fragment
 */
function _renderJsonObj(obj, state) {
  state = state || {};
  var parts = [];
  if (!(state.copilotToolKeys instanceof Set)) state.copilotToolKeys = new Set();
  if (typeof state.copilotDeltaBuffer !== 'string') state.copilotDeltaBuffer = '';
  if (typeof state.copilotReasoningPending !== 'boolean') state.copilotReasoningPending = false;

  function assistantBubbleHtml(text) {
    return '<div style="display:flex;align-items:baseline;gap:6px;margin:4px 0">' +
      '<span style="color:var(--muted);font-size:var(--text-sm);flex-shrink:0">&#9679;</span>' +
      '<div style="background:var(--surface2);padding:8px 12px;border-radius:12px 12px 12px 2px;max-width:90%;font-size:var(--text-md);word-break:break-word">' + renderMd(text) + '</div>' +
      '</div>';
  }
  function toolUseHtml(name, input) {
    var summary = formatToolSummary(name || 'tool', input || {});
    var rawJson = escHtml(JSON.stringify(input || {}, null, 2).slice(0, 500));
    return '<div style="display:flex;align-items:center;gap:4px;margin:2px 0;font-size:var(--text-sm);color:var(--muted);font-family:monospace">' +
        '<span style="flex-shrink:0">&#9679;</span>' +
        '<span>' + summary + '</span>' +
        '<span style="cursor:pointer;opacity:0.6;margin-left:4px" onclick="var t=this.parentElement.nextElementSibling;t.style.display=t.style.display===\'none\'?\'block\':\'none\';this.textContent=t.style.display===\'none\'?\'[+]\':\'[-]\'">[+]</span>' +
      '</div>' +
      '<div style="display:none;background:var(--bg);padding:4px 8px;border-radius:4px;margin:0 0 4px 16px;font-size:var(--text-sm);font-family:monospace;white-space:pre-wrap;max-height:200px;overflow-y:auto;color:var(--muted)">' + rawJson + '</div>';
  }
  function toolResultHtml(text) {
    if (!text || text.length <= 10) return '';
    var truncated = text.length > 3000;
    var displayText = truncated ? text.slice(0, 3000) + '...' : text;
    return '<div style="background:var(--surface);border-left:2px solid var(--border);padding:2px 8px;margin:0 0 2px 16px;font-size:var(--text-xs);font-family:monospace;color:var(--muted);max-height:160px;overflow-y:auto;white-space:pre-wrap;cursor:pointer" onclick="this.style.maxHeight=this.style.maxHeight===\'160px\'?\'none\':\'160px\'">' + escHtml(displayText) + '</div>';
  }
  function toolKey(name, input) {
    try { return String(name || 'tool') + '|' + JSON.stringify(input || {}); }
    catch { return String(name || 'tool'); }
  }

  if (obj.type === 'assistant' && obj.message && obj.message.content) {
    var content = obj.message.content;
    for (var i = 0; i < content.length; i++) {
      var block = content[i];
      if (block.type === 'thinking') {
        parts.push('<div style="font-size:var(--text-sm);color:var(--muted);padding:2px 8px;font-style:italic">\u{1F4AD} Thinking...</div>');
      }
      if (block.type === 'text' && block.text) {
        parts.push(assistantBubbleHtml(block.text));
      }
      if (block.type === 'tool_use') {
        parts.push(toolUseHtml(block.name, block.input));
      }
    }
  }

  if (obj.type === 'tool_result' || (obj.type === 'user' && obj.message && obj.message.content && obj.message.content[0] && obj.message.content[0].type === 'tool_result')) {
    var tc = (obj.message && obj.message.content && obj.message.content[0] && obj.message.content[0].content) || obj.content || '';
    var text = typeof tc === 'string' ? tc : JSON.stringify(tc);
    parts.push(toolResultHtml(text));
  }

  if (obj.type === 'assistant.reasoning' || obj.type === 'assistant.reasoning_delta') {
    state.copilotReasoningPending = true;
  }

  if (obj.type === 'assistant.message_delta' && typeof obj.data?.deltaContent === 'string') {
    if (state.copilotReasoningPending) {
      parts.push('<div style="font-size:var(--text-sm);color:var(--muted);padding:2px 8px;font-style:italic">\u{1F4AD} Thinking...</div>');
      state.copilotReasoningPending = false;
    }
    state.copilotDeltaBuffer += obj.data.deltaContent;
  }

  if (obj.type === 'assistant.message') {
    if (state.copilotReasoningPending) {
      parts.push('<div style="font-size:var(--text-sm);color:var(--muted);padding:2px 8px;font-style:italic">\u{1F4AD} Thinking...</div>');
      state.copilotReasoningPending = false;
    }
    state.copilotDeltaBuffer = '';
    if (typeof obj.data?.content === 'string' && obj.data.content) {
      parts.push(assistantBubbleHtml(obj.data.content));
    }
    var toolRequests = Array.isArray(obj.data?.toolRequests) ? obj.data.toolRequests : [];
    for (var trIdx = 0; trIdx < toolRequests.length; trIdx++) {
      var tr = toolRequests[trIdx];
      if (!tr || !tr.name) continue;
      var trInput = tr.arguments || {};
      var trKey = toolKey(tr.name, trInput);
      if (state.copilotToolKeys.has(trKey)) continue;
      state.copilotToolKeys.add(trKey);
      parts.push(toolUseHtml(tr.name, trInput));
    }
  }

  if (obj.type === 'tool.execution_start' && obj.data?.toolName) {
    var startInput = obj.data.arguments || {};
    var startKey = toolKey(obj.data.toolName, startInput);
    if (!state.copilotToolKeys.has(startKey)) {
      state.copilotToolKeys.add(startKey);
      parts.push(toolUseHtml(obj.data.toolName, startInput));
    }
  }

  if (obj.type === 'tool.execution_complete') {
    var resultData = obj.data?.result;
    var resultText = resultData?.content || resultData?.detailedContent || '';
    if (!resultText && resultData && typeof resultData !== 'string') resultText = JSON.stringify(resultData);
    if (!resultText && obj.data?.success === false) resultText = 'Tool failed';
    parts.push(toolResultHtml(typeof resultText === 'string' ? resultText : String(resultText || '')));
  }

  if (obj.type === 'result') {
    // W-mpob4nyk0006580e — the terminal banner is no longer derived here.
    // The engine's dispatch.result is the single source of truth and is
    // pushed to the live view via the SSE `dispatch.terminal` channel
    // (engine/dispatch-events.js → dashboard.js handleAgentLiveStream).
    // `result` JSONL lines and the `[process-exit]` sentinel are still
    // parsed for other purposes (e.g. tool deltas above), but they must
    // NOT trigger any banner here — log heuristics can disagree with the
    // engine verdict (CLI crash AFTER successful completion report, exit-0
    // with no result line, timeout kill, …). See renderTerminalBanner().
  }

  return parts.join('');
}

/**
 * Takes raw JSONL agent output text and returns an HTML string with formatted rendering.
 * Parses JSON objects, tool summaries with ● prefix and [+] toggles for raw JSON,
 * tool results with surface tint and 160px max-height, assistant text with ● prefix
 * and markdown rendering, steering bubbles with ❯ prefix, completion indicator,
 * stderr lines, and heartbeat filtering.
 *
 * @param {string} text - Raw JSONL agent output
 * @returns {string} HTML string
 */
function renderAgentOutput(text) {
  if (!text) return '';
  var fragments = [];
  var lines = text.split('\n');
  var state = { copilotDeltaBuffer: '', copilotToolKeys: new Set(), copilotReasoningPending: false };

  function flushCopilotPending() {
    if (state.copilotReasoningPending) {
      fragments.push('<div style="font-size:var(--text-sm);color:var(--muted);padding:2px 8px;font-style:italic">\u{1F4AD} Thinking...</div>');
      state.copilotReasoningPending = false;
    }
    if (state.copilotDeltaBuffer) {
      fragments.push('<div style="display:flex;align-items:baseline;gap:6px;margin:4px 0">' +
        '<span style="color:var(--muted);font-size:var(--text-sm);flex-shrink:0">&#9679;</span>' +
        '<div style="background:var(--surface2);padding:8px 12px;border-radius:12px 12px 12px 2px;max-width:90%;font-size:var(--text-md);word-break:break-word">' + renderMd(state.copilotDeltaBuffer) + '</div>' +
        '</div>');
      state.copilotDeltaBuffer = '';
    }
  }

  // W-mpob4nyk0006580e — pre-scan removed. Previously we found the final
  // `"type":"result"` line + `[process-exit]` sentinel to gate a terminal
  // banner here, but the terminal banner is now pushed from the engine via
  // the `dispatch.terminal` SSE channel (see renderTerminalBanner()). The
  // log lines below are still parsed for tool calls / assistant text /
  // reasoning deltas — only the banner-gating heuristic is gone.

  for (var i = 0; i < lines.length; i++) {
    var trimmed = lines[i].trim();
    if (!trimmed || trimmed.startsWith('#')) continue;

    // Heartbeat — filter out
    if (trimmed.startsWith('[heartbeat]')) continue;

    // Process-exit sentinel — internal signal, never displayed
    if (trimmed.startsWith('[process-exit]')) continue;

    // Human steering
    if (trimmed.startsWith('[human-steering]')) {
      var msg = trimmed.replace('[human-steering] ', '');
      fragments.push('<div style="align-self:flex-end;background:var(--blue);color:#fff;padding:6px 12px;border-radius:12px 12px 2px 12px;max-width:80%;margin:4px 0;font-size:var(--text-md)">' +
        '<span style="margin-right:4px">❯</span>' + escHtml(msg) +
        '<div style="font-size:var(--text-xs);opacity:0.7;margin-top:2px">\u2713 Queued</div></div>');
      continue;
    }

    // Steering failed
    if (trimmed.startsWith('[steering-failed]')) {
      var failMsg = trimmed.replace('[steering-failed] ', '');
      fragments.push('<div style="background:rgba(248,81,73,0.1);border:1px solid var(--red);color:var(--red);padding:6px 12px;border-radius:8px;margin:4px 0;font-size:var(--text-base)">\u26A0 ' + escHtml(failMsg) + '</div>');
      continue;
    }

    // JSON array line
    if (trimmed.startsWith('[')) {
      try {
        var arr = JSON.parse(trimmed);
        if (Array.isArray(arr)) {
          for (var j = 0; j < arr.length; j++) fragments.push(_renderJsonObj(arr[j], state));
          continue;
        }
      } catch (e) { /* fall through */ }
    }

    // JSON object line
    if (trimmed.startsWith('{')) {
      try {
        var obj = JSON.parse(trimmed);
        if (obj.type !== 'assistant.message_delta' && obj.type !== 'assistant.reasoning' && obj.type !== 'assistant.reasoning_delta' && obj.type !== 'assistant.message') {
          flushCopilotPending();
        }
        fragments.push(_renderJsonObj(obj, state));
        continue;
      } catch (e) { /* fall through */ }
    }

    flushCopilotPending();

    // Stderr
    if (trimmed.startsWith('[stderr]')) {
      fragments.push('<div style="font-size:var(--text-xs);color:var(--red);font-family:monospace;padding:1px 4px">' + escHtml(trimmed) + '</div>');
    } else {
      // Plain text fallback
      fragments.push('<div style="font-size:var(--text-sm);color:var(--muted);font-family:monospace;padding:1px 4px">' + escHtml(trimmed) + '</div>');
    }
  }

  flushCopilotPending();

  // W-mpob4nyk0006580e — no log-derived banner here either. The fallback
  // "exit non-zero with no result line" branch used to render a red banner
  // for crashes mid-write; that case is now covered by the engine's
  // `dispatch.result` (set by completeDispatch and pushed via the SSE
  // `dispatch.terminal` channel — see renderTerminalBanner()). Removing
  // this branch is what makes the live view banner agree with the agent
  // row's status in cases like exit-1 AFTER a success completion report.
  return fragments.join('');
}

/**
 * W-mpob4nyk0006580e — render the terminal banner from an authoritative
 * `dispatch.terminal` SSE event. The shape mirrors
 * engine/dispatch-events.js buildTerminalEvent():
 *   { type, ts, dispatchId, agentId, result, reason, failureClass, prUrl, summary }
 *
 * The banner state mirrors `dispatch.result` exactly — green for SUCCESS,
 * red otherwise. Optional reason / failure class / PR link / summary first
 * line are surfaced as secondary context.
 *
 * Returns HTML for a single banner div. Callers replace the banner
 * container's innerHTML on each event.
 */
function renderTerminalBanner(event) {
  if (!event || !event.result) return '';
  var isSuccess = event.result === 'success';
  var color = isSuccess ? 'var(--green)' : 'var(--red)';
  var bg = isSuccess ? 'rgba(63,185,80,0.1)' : 'rgba(248,81,73,0.1)';
  var glyph = isSuccess ? '\u2713' : '\u2717';
  var headline;
  if (isSuccess) {
    headline = glyph + ' Task complete';
  } else {
    // 'error' / 'timeout' / anything-else → use reason word from the engine
    var reasonWord = event.result === 'timeout' ? 'timeout' : (event.failureClass || 'error');
    headline = glyph + ' Task ended with ' + escHtml(String(reasonWord));
  }
  var extra = '';
  if (event.summary) extra += '<div style="font-size:var(--text-base);margin-top:4px;opacity:0.9">' + escHtml(event.summary) + '</div>';
  if (event.reason && event.reason !== event.summary) {
    extra += '<div style="font-size:var(--text-sm);margin-top:2px;opacity:0.75">' + escHtml(event.reason) + '</div>';
  }
  if (event.prUrl) {
    var safeUrl = encodeURI(String(event.prUrl));
    extra += '<div style="font-size:var(--text-base);margin-top:4px"><a href="' + escHtml(safeUrl) + '" target="_blank" rel="noopener" style="color:' + color + '">' + escHtml(event.prUrl) + '</a></div>';
  }
  return '<div style="background:' + bg + ';border:1px solid ' + color + ';padding:8px 12px;border-radius:8px;margin:8px 0;font-size:var(--text-md);color:' + color + '">' +
    headline + extra +
    '</div>';
}

/**
 * Standard "Prev / Next" pager HTML for a paginated list. Used by inbox, KB,
 * completed dispatches, and engine log. The caller is responsible for the
 * `onPrev`/`onNext` JS expression strings — they're inlined as `onclick`
 * attributes, so callers must pass safe identifier-only expressions
 * (e.g. `'_inboxPrev()'`).
 *
 * @param {object} opts
 * @param {number} opts.start - Zero-based start index of the current page.
 * @param {number} opts.perPage - Items per page.
 * @param {number} opts.total - Total items across all pages.
 * @param {number} opts.page - Zero-based current page index.
 * @param {number} opts.totalPages - Total page count.
 * @param {string} opts.onPrev - JS expression for the Prev button's onclick.
 * @param {string} opts.onNext - JS expression for the Next button's onclick.
 * @returns {string} HTML string for the pager (empty when total <= perPage).
 */
function renderPager(opts) {
  var start = opts.start, perPage = opts.perPage, total = opts.total;
  var page = opts.page, totalPages = opts.totalPages;
  if (total <= perPage) return '';
  return '<div class="pr-pager">' +
    '<span class="pr-page-info">Showing ' + (start + 1) + ' to ' + Math.min(start + perPage, total) + ' of ' + total + '</span>' +
    '<div class="pr-pager-btns">' +
      '<button class="pr-pager-btn ' + (page === 0 ? 'disabled' : '') + '" onclick="' + opts.onPrev + '">Prev</button>' +
      '<button class="pr-pager-btn ' + (page >= totalPages - 1 ? 'disabled' : '') + '" onclick="' + opts.onNext + '">Next</button>' +
    '</div></div>';
}

/**
 * Renders the standard Pin/Unpin chip used in the inbox and KB lists. Both
 * call _togglePinAndRefresh(pinKey, source) on click; the only thing that
 * varies is the source name. The data-pin-key attribute is escaped via
 * escHtml; callers must pass the already-computed pin key.
 *
 * @param {string} pinKey - The pin storage key (e.g. inboxPinKey(name)).
 * @param {boolean} pinned - Current pinned state.
 * @param {string} source - The source name passed to _togglePinAndRefresh
 *   (e.g. 'inbox', 'kb').
 * @param {object} [opts]
 * @param {string} [opts.extraStyle] - Inline CSS appended to the button.
 * @returns {string} HTML for the pin button.
 */
function pinButton(pinKey, pinned, source, opts) {
  var extraStyle = (opts && opts.extraStyle) || '';
  return '<button class="pr-pager-btn pin-btn' + (pinned ? ' pinned' : '') +
    '" style="font-size:var(--text-xs);padding:2px 8px;' + extraStyle +
    '" data-pin-key="' + escHtml(pinKey) +
    '" onclick="event.stopPropagation();_togglePinAndRefresh(this.dataset.pinKey,\'' + source + '\')">' +
    (pinned ? 'Unpin' : 'Pin') + '</button>';
}

function _scrollNodesForSelector(root, selector) {
  if (!root || !selector) return [];
  if (selector === ':self') return [root];
  var out = [];
  if (root.matches && root.matches(selector)) out.push(root);
  if (root.querySelectorAll) {
    root.querySelectorAll(selector).forEach(function(el) { out.push(el); });
  }
  return out;
}

function captureDashboardScrollState(root, selectors) {
  var state = [];
  (selectors || []).forEach(function(selector) {
    var nodes = _scrollNodesForSelector(root, selector);
    nodes.forEach(function(el, index) {
      state.push({ selector: selector, index: index, top: el.scrollTop || 0, left: el.scrollLeft || 0 });
    });
  });
  return state;
}

function restoreDashboardScrollState(root, state) {
  (state || []).forEach(function(item) {
    var nodes = _scrollNodesForSelector(root, item.selector);
    var el = nodes[item.index];
    if (!el) return;
    el.scrollTop = item.top || 0;
    el.scrollLeft = item.left || 0;
  });
}

window.MinionsRenderUtils = { formatToolSummary, renderAgentOutput, renderTerminalBanner, renderPager, pinButton, captureDashboardScrollState, restoreDashboardScrollState };
