<%
  // Read the trace off the profile, not the collector snapshot (`data`): buildTrace() runs after
  // collectAll() snapshotted this panel, so `data` is empty here — profile.trace is the source.
  // One model, two lanes: the causal waterfall and the flat framework band above it.
  const allSpans = profile.trace || [];
  const spans = allSpans.filter((s) => s.lane !== 'lifecycle');
  const lifecycle = allSpans
    .filter((s) => s.lane === 'lifecycle')
    .map((s) => ({ name: s.label, startedAt: s.startedAt, duration: s.duration }));
  const startTime = profile.performance.startTime;
  const totalDur = profile.performance.duration || 1;

  // Colour + label per span kind. Inline (like the former phase map) so the single
  // shipped stylesheet needs no per-kind utility classes.
  const kindColors = {
    entrypoint:      'var(--color-nest)',
    phase:           '#6366f1',
    method:          '#a78bfa',
    http:            '#06b6d4',
    db:              '#10b981',
    'graphql-field': '#8b5cf6',
    custom:          '#64748b',
  };
  const kindLabels = {
    entrypoint: 'Request',
    phase: 'Phase',
    method: 'Method',
    http: 'HTTP',
    db: 'Database',
    'graphql-field': 'GraphQL',
    custom: 'Custom',
  };
  // What each kind *is*, so the nesting reads unambiguously: containers group the work
  // happening inside them, leaves are the actual I/O. Only leaves consume time on their
  // own — a container's own cost is its self-time.
  const kindHints = {
    entrypoint: 'The profiled request, command or message — the whole time budget.',
    phase: 'A section of your code, opened with tracer.span(). It groups the work it triggered; its own cost is its self-time.',
    method: 'One provider method call, recorded by the automatic instrumentation — who called whom. Its own cost is its self-time.',
    http: 'One outgoing HTTP call — a leaf, timed from send to response.',
    db: 'One database statement — a leaf. A "transaction" row groups a BEGIN … COMMIT run.',
    'graphql-field': 'One resolved GraphQL field; it groups the calls that resolving it issued.',
    custom: 'A span contributed by a custom collector.',
  };
  function kindHint(kind) { return kindHints[kind] || ''; }
  function kindColor(kind) { return kindColors[kind] || kindColors.custom; }
  function kindLabel(kind) { return kindLabels[kind] || kind; }

  // Rebuild the tree from the flat, parentId-linked array, then flatten it depth-first
  // so rows render in causal order with an indentation level.
  const byId = new Map(spans.map((s) => [s.id, s]));
  const children = new Map();
  const roots = [];
  for (const s of spans) {
    const parent = s.parentId && byId.has(s.parentId) ? s.parentId : null;
    if (parent) {
      if (!children.has(parent)) children.set(parent, []);
      children.get(parent).push(s);
    } else {
      roots.push(s);
    }
  }
  // buildTrace already emits spans chronologically (emission order breaking ties), so the
  // flat array order is the causal order — re-sorting on duration would put a 0ms COMMIT
  // ahead of the START TRANSACTION it closes.
  // Wall-clock time the children actually cover, as the union of their intervals — not the sum
  // of their durations. Children overlap all the time (calls fired together, a phase and the
  // query it wraps reported side by side), and summing would count that overlap twice and
  // collapse every self-time to 0.
  function coveredBy(kids) {
    const intervals = kids
      .map((k) => [k.startedAt, k.startedAt + k.duration])
      .sort((a, b) => a[0] - b[0]);
    let covered = 0;
    let [start, end] = [0, -Infinity];
    for (const [from, to] of intervals) {
      if (from > end) {
        covered += Math.max(end - start, 0);
        [start, end] = [from, to];
      } else if (to > end) {
        end = to;
      }
    }
    return covered + Math.max(end - start, 0);
  }

  const rows = [];
  function walk(node, depth) {
    const kids = children.get(node.id) || [];
    // Self-time: the stretch of this span no child accounts for — waiting, driver/ORM
    // overhead, application code between two calls.
    const selfTime = Math.max(node.duration - coveredBy(kids), 0);
    rows.push({ span: node, depth, hasChildren: kids.length > 0, selfTime });
    for (const kid of kids) walk(kid, depth + 1);
  }
  for (const root of roots) walk(root, 0);

  // Critical path: from the root, at each level follow the child that finishes last. That chain is
  // what decided the total — shortening anything off it moves nothing. Computed here, where the
  // tree is already built, rather than client-side over the DOM.
  const criticalIds = new Set();
  (function walkCritical(node) {
    if (!node) return;
    criticalIds.add(node.id);
    const kids = children.get(node.id) || [];
    if (!kids.length) return;
    let last = kids[0];
    for (const kid of kids) {
      if (kid.startedAt + kid.duration > last.startedAt + last.duration) last = kid;
    }
    walkCritical(last);
  })(roots[0]);

  const usedKinds = [...new Set(spans.map((s) => s.kind))];

  // Automatic instrumentation records one span per provider method call, which is exactly the
  // information you want when asking "who issued this query" — and exactly the noise you do not
  // want when asking "which query is slow". A lens rather than a second panel: same tree, same
  // ids, three readings. Rows are hidden by the client behaviour, so switching costs no round trip
  // and a hidden row's children stay attached to their real parent.
  const hasMethodSpans = usedKinds.includes('method');

  // Flat request-lifecycle band (Symfony-style), rendered above the causal waterfall on the same
  // time axis. Non-nested, so it never competes with the trace's containment nesting.
  const lifecycleColors = { guards: '#f59e0b', validation: '#f97316', controller: '#8b5cf6' };
%>

<% if (lifecycle.length) { %>
  <div class="mb-6">
    <h2 class="text-foreground-muted text-2xs uppercase tracking-widest font-semibold mb-2">Request Lifecycle</h2>
    <div class="space-y-1.5">
      <% for (const ph of lifecycle) { %>
        <%
          const lOffset = Math.min(Math.max(((ph.startedAt - startTime) / totalDur) * 100, 0), 100);
          const lWidth = Math.min(Math.max((ph.duration / totalDur) * 100, 0.5), 100 - lOffset);
          const lColor = lifecycleColors[ph.name] || '#64748b';
        %>
<%# Same column geometry as a trace row below (label / track / badges / duration), so both
    bands really do read against one time axis. %>
        <div class="flex items-center gap-3">
          <div class="w-56 shrink-0 text-foreground-secondary text-xs capitalize truncate" title="<%= ph.name %>"><%= ph.name %></div>
          <div class="flex-1 relative h-4 bg-surface-muted border border-line rounded overflow-hidden">
            <div class="absolute top-0 h-full rounded" style="left:<%= lOffset.toFixed(2) %>%;width:<%= lWidth.toFixed(2) %>%;background:<%= lColor %>;opacity:0.85;min-width:2px;"></div>
          </div>
          <div class="w-24 shrink-0 hidden sm:block"></div>
          <div class="w-20 shrink-0 text-right text-foreground-secondary text-xs tabular-nums"><%= formatDuration(ph.duration) + 'ms' %></div>
        </div>
      <% } %>
    </div>
  </div>
<% } %>

<div class="mb-6">
  <div class="flex items-center justify-between mb-2">
    <h2 class="text-foreground-muted text-2xs uppercase tracking-widest font-semibold">Execution Trace</h2>
    <span class="text-foreground-faint text-xs tabular-nums">
      <span data-trace-count hidden></span><%= formatDuration(totalDur) + 'ms' %> total
    </span>
  </div>

  <% if (spans.length === 0) { %>
    <div class="py-8 text-center bg-surface-muted border border-line rounded-lg">
      <p class="text-foreground-muted text-sm mb-2">No spans recorded.</p>
      <p class="text-foreground-faint text-xs">Outgoing HTTP calls and database queries appear here automatically; use <code class="bg-surface border border-line px-1.5 py-0.5 rounded">tracer.span('name', work)</code> to add your own.</p>
    </div>
  <% } else { %>
    <div class="flex flex-wrap items-center gap-2 mb-3">
      <button type="button"
              class="trace-critical px-2 py-0.5 rounded border border-line text-2xs text-foreground-muted hover:text-foreground transition-colors"
              data-trace-critical
              title="Dim everything off the chain that decided the total duration. Shortening a span off it moves nothing."
              aria-pressed="false">Critical path</button>
<%# Hiding short spans is a *display* choice, so it lives here rather than at capture: a 0.04ms
    parent can still contain 12ms of work, and dropping it at record time would lose that. %>
      <label class="inline-flex items-center gap-1.5 text-2xs text-foreground-muted">
        Hide under
        <select data-trace-min class="bg-surface border border-line rounded px-1 py-0.5 text-2xs">
          <% for (const ms of [0, 0.1, 0.5, 1, 5]) { %>
            <option value="<%= ms %>"<%= ms === minDuration ? ' selected' : '' %>><%= ms === 0 ? 'nothing' : ms + 'ms' %></option>
          <% } %>
        </select>
      </label>
    </div>

    <% if (hasMethodSpans) { %>
      <div class="flex items-center gap-2 mb-3" data-trace-lens-group>
        <span class="text-2xs text-foreground-faint uppercase tracking-widest mr-1">Show</span>
        <% for (const lens of [
             { key: 'all', label: 'All', hint: 'Every span, code and I/O alike.' },
             { key: 'io', label: 'I/O only', hint: 'Hide provider method calls — what the request spent its time waiting on.' },
             { key: 'code', label: 'Code only', hint: 'Only your provider methods — who called whom.' },
           ]) { %>
          <button type="button"
                  class="trace-lens px-2 py-0.5 rounded border border-line text-2xs text-foreground-muted hover:text-foreground transition-colors"
                  data-trace-lens="<%= lens.key %>"
                  title="<%= lens.hint %>"><%= lens.label %></button>
        <% } %>
      </div>
    <% } %>

    <% if (usedKinds.length > 1) { %>
      <div class="flex flex-wrap items-center gap-x-4 gap-y-1 mb-3">
        <% for (const kind of usedKinds) { %>
          <span class="inline-flex items-center gap-1.5 text-2xs text-foreground-muted cursor-help" title="<%= kindHint(kind) %>">
            <span class="inline-block w-2.5 h-2.5 rounded-sm" style="background:<%= kindColor(kind) %>;"></span>
            <%= kindLabel(kind) %>
          </span>
        <% } %>
      </div>
    <% } %>

    <div class="space-y-1">
      <% for (const row of rows) { %>
        <%
          const span = row.span;
          const offsetPct = Math.min(Math.max(((span.startedAt - startTime) / totalDur) * 100, 0), 100);
          const widthPct = Math.min(Math.max((span.duration / totalDur) * 100, 0.5), 100 - offsetPct);
          const color = kindColor(span.kind);
          const indent = row.depth * 14;
          const isError = span.status === 'error';
          const source = span.source;
          const tab = source && source.tab;
          const labelClass = 'text-xs truncate min-w-0 flex-1 ' + (isError ? 'text-danger' : 'text-foreground-secondary');
        %>
        <div class="flex items-center gap-3 group"
             data-trace-kind="<%= span.kind %>"
             data-trace-duration="<%= span.duration %>"
             <% if (criticalIds.has(span.id)) { %>data-trace-critical-node<% } %>
             data-trace-node="<%= span.id %>"
             <% if (span.parentId) { %>data-trace-parent="<%= span.parentId %>"<% } %>>
          <div class="w-56 shrink-0 flex items-center gap-1" style="padding-left:<%= indent %>px;">
            <% if (row.hasChildren) { %>
              <button type="button"
                      class="trace-caret shrink-0 w-4 h-4 flex items-center justify-center text-foreground-faint hover:text-foreground"
                      style="transform:rotate(90deg);transition:transform .15s;"
                      data-trace-toggle="<%= span.id %>"
                      aria-label="Toggle children">
                <svg viewBox="0 0 16 16" fill="currentColor" class="w-3 h-3"><path d="M6 4l4 4-4 4z"/></svg>
              </button>
            <% } else { %>
              <span class="shrink-0 w-4"></span>
            <% } %>
            <span class="inline-block w-1.5 h-1.5 rounded-full shrink-0" style="background:<%= color %>;"></span>
            <% if (source) { %>
              <button type="button" class="<%= labelClass %> text-left hover:text-nest transition-colors cursor-pointer"
                      data-trace-detail="<%= span.id %>" title="<%= span.label %>"><%= span.label %></button>
            <% } else { %>
              <span class="<%= labelClass %>" title="<%= span.label %>"><%= span.label %></span>
            <% } %>
          </div>
          <div class="flex-1 relative h-5 bg-surface-muted border border-line rounded overflow-hidden">
            <div class="absolute top-0 h-full rounded"
                 style="left:<%= offsetPct.toFixed(2) %>%;width:<%= widthPct.toFixed(2) %>%;background:<%= color %>;opacity:<%= isError ? '0.95' : '0.85' %>;min-width:2px;<%= isError ? 'outline:1px solid var(--color-danger);' : '' %>">
            </div>
          </div>
<%# Fixed width, rendered even when empty: the track above is flex-1, so a badge appearing on
    some rows only would shorten their time axis and shift their bar against the rows around it. %>
          <div class="w-24 shrink-0 hidden sm:flex items-center justify-end gap-1 overflow-hidden">
            <% if (span.tags && span.tags.length && typeof tagBadges === 'function') { %><%- tagBadges(span.tags) %><% } %>
          </div>
          <div class="w-20 shrink-0 text-right text-xs tabular-nums leading-tight whitespace-nowrap">
            <div class="text-foreground-secondary"><%= formatDuration(span.duration) + 'ms' %></div>
            <% if (row.hasChildren && row.selfTime > 0 && row.selfTime < span.duration) { %>
              <div class="text-2xs text-foreground-faint cursor-help"
                   title="Self time — <%= formatDuration(row.selfTime) + 'ms' %> of this span's <%= formatDuration(span.duration) + 'ms' %> is not covered by its children: waiting, driver or ORM overhead, application code in between.">
                self <%= formatDuration(row.selfTime) + 'ms' %>
              </div>
            <% } %>
          </div>
        </div>
        <% if (source) { %>
          <div class="hidden" data-trace-detail-panel="<%= span.id %>">
            <div class="rounded border border-line bg-surface-muted p-3" style="margin-left:<%= indent + 24 %>px;">
              <% if (span.kind === 'db' && typeof highlightSql === 'function') { %>
                <pre class="overflow-x-auto m-0 mb-2 text-xs"><code class="text-code-text"><%- highlightSql(span.label) %></code></pre>
              <% } else { %>
                <p class="break-all m-0 mb-2 text-xs text-foreground-secondary"><%= span.label %></p>
              <% } %>
              <% if (span.meta && Object.keys(span.meta).length) { %>
                <div class="flex flex-wrap gap-x-4 gap-y-1 text-2xs text-foreground-muted mb-2">
                  <% for (const [k, v] of Object.entries(span.meta)) { %>
                    <span><span class="text-foreground-faint"><%= k %>:</span> <%= v %></span>
                  <% } %>
                </div>
              <% } %>
              <% if (tab) {
                // Deep-link to the source panel: the grouped Database tab needs its sub-tab
                // (typeorm/mongoose) selected, plus the query index so the client highlights the row.
                const collector = source.collector;
                let href = '?tab=' + encodeURIComponent(tab);
                if (collector && collector !== tab) href += '&subtab=' + encodeURIComponent(collector);
                if (source.index != null) href += '&q=' + source.index;
              %>
                <a href="<%= href %>" class="text-2xs text-nest hover:underline">Open in <%= kindLabel(span.kind) %> panel →</a>
              <% } %>
            </div>
          </div>
        <% } %>
      <% } %>
    </div>

    <div class="mt-6 rounded-lg border border-line overflow-x-auto">
      <table class="w-full min-w-[560px]">
        <thead>
          <tr class="bg-surface-muted border-b border-line">
            <th class="text-left py-2.5 px-4 text-foreground-muted font-medium text-2xs uppercase tracking-widest">Span</th>
            <th class="text-left py-2.5 px-4 text-foreground-muted font-medium text-2xs uppercase tracking-widest cursor-help" title="Request, Phase and GraphQL group the work under them; HTTP and Database are the leaves that spend the time.">Kind</th>
            <th class="text-left py-2.5 px-4 text-foreground-muted font-medium text-2xs uppercase tracking-widest">Started at</th>
            <th class="text-right py-2.5 px-4 text-foreground-muted font-medium text-2xs uppercase tracking-widest">Duration</th>
            <th class="text-right py-2.5 px-4 text-foreground-muted font-medium text-2xs uppercase tracking-widest cursor-help" title="Share of the total request time. Containers overlap their children and concurrent spans overlap each other, so these do not add up to 100%.">% of total</th>
          </tr>
        </thead>
        <tbody class="divide-y divide-line-subtle">
          <% for (const row of rows) { %>
            <% const span = row.span; %>
<%# Tagged like the bar above it, and with the same span id: the table is the *same* trace read as
    a list, so a lens or a fold that hides a bar must hide its row too — otherwise "I/O only" still
    lists every method underneath, which is exactly the contradiction this pairing removes. %>
            <tr class="hover:bg-surface-muted transition-colors"
                data-trace-row="<%= span.id %>"
                data-trace-kind="<%= span.kind %>"
                data-trace-duration="<%= span.duration %>">
              <td class="py-2 px-4 text-foreground text-xs font-medium">
                <span style="padding-left:<%= row.depth * 14 %>px;" class="inline-flex items-center gap-1.5">
                  <span class="inline-block w-1.5 h-1.5 rounded-full" style="background:<%= kindColor(span.kind) %>;"></span>
                  <span class="truncate max-w-[280px] inline-block align-bottom <%= span.status === 'error' ? 'text-danger' : '' %>" title="<%= span.label %>"><%= span.label %></span>
                </span>
              </td>
              <td class="py-2 px-4 text-foreground-muted text-xs cursor-help" title="<%= kindHint(span.kind) %>"><%= kindLabel(span.kind) %></td>
              <td class="py-2 px-4 text-foreground-muted text-xs tabular-nums"><%= timeOnly(span.startedAt) %></td>
              <td class="py-2 px-4 text-right text-foreground-secondary text-xs tabular-nums"><%= formatDuration(span.duration) + 'ms' %></td>
              <td class="py-2 px-4 text-right text-foreground-faint text-xs tabular-nums"><%= ((span.duration / totalDur) * 100).toFixed(1) %>%</td>
            </tr>
          <% } %>
        </tbody>
      </table>
    </div>
  <% } %>
</div>
