// JobqRollup.tsx — <JobqRollup>, a compact one-line banner over
// GET .../api/jobq/rollup's pre-tallied `Vec<hive_jobq_wire::StateCount>`
// (any endpoint serving that shape works — hive-c0re and swarm-controller
// both do). Extracted out of dashboard/src/swarm.js's hand-rolled
// queue-summary banner, which this component now replaces there too —
// same "one shared component, not two parallel implementations" pattern
// `JobqGraph` already established for the rebuild-queue tree view.
//
// Reads `roots` (not `nodes`) — "N running / M queued" has always meant
// *operations*, not raw steps (one rebuild is ~7 nodes but 1 root).
// `Finishing` counts as running (own work done, subtree still going,
// still in flight) — the rollup endpoint doesn't collapse the two
// itself, so this component does. Renders nothing at all when there's
// no active work, matching the banner it replaces (an idle queue is not
// worth a line of chrome).
//
// `queueHref`, when given, adds a "view queue →" link (dashboard links to
// /builds.html; swarm-ui's /jobs page omits it — a link to the page
// you're already on is noise).
//
// The glyph carries `.spinner` (`@hive/shared/base.css`, imported by
// both consumers) for the "actively happening" spin — a static
// screenshot can't tell a frozen spinner from a missing one.
//
// JSX (swarm-ui) — `<JobqRollup endpoint="..." queueHref="..." />`. Or
// plain `render(h(JobqRollup, props), container)` (dashboard/src/
// swarm.js, no JSX pragma needed) — call again with a bumped
// `refreshToken` to force a refetch. No mount wrapper: `render` is
// already the re-render/diff entry point.

import { useState, useEffect } from 'preact/hooks';

// Mirrors `hive_jobq_wire::StateSchema` — only the subset this banner
// cares about, not the full union `JobqGraph.tsx` mirrors, since a
// rollup row's `state` is read by exact string match, not rendered.
type NodeState = 'Pending' | 'Running' | 'Finishing' | 'Done' | 'Failed' | 'Cancelled' | 'Skipped';

interface StateCount {
  state: NodeState;
  nodes: number;
  roots: number;
}

export interface JobqRollupProps {
  endpoint?: string;
  queueHref?: string;
  refreshToken?: number;
}

export function JobqRollup({ endpoint, queueHref, refreshToken = 0 }: JobqRollupProps) {
  const [counts, setCounts] = useState<StateCount[]>([]);

  useEffect(() => {
    if (!endpoint) return undefined;
    let cancelled = false;
    // Best-effort: a failed fetch leaves the previous snapshot in
    // place rather than wiping the banner on a network blip — same
    // rule the hand-rolled version followed.
    (async () => {
      try {
        const r = await fetch(endpoint);
        if (!r.ok) return;
        const data = (await r.json()) as StateCount[];
        if (!cancelled) setCounts(data);
      } catch {
        // ignore — keep the previous snapshot
      }
    })();
    return () => { cancelled = true; };
  }, [endpoint, refreshToken]);

  const byState = new Map(counts.map((c) => [c.state, c]));
  const running = (byState.get('Running')?.roots ?? 0) + (byState.get('Finishing')?.roots ?? 0);
  const queued = byState.get('Pending')?.roots ?? 0;
  if (!running && !queued) return null;

  const parts: string[] = [];
  if (running) parts.push(`${running} running`);
  if (queued) parts.push(`${queued} queued`);

  return (
    <div class="jqr-summary">
      <span class="jqr-glyph spinner">◐</span>{' '}
      <strong>build queue</strong> — {parts.join(' · ')}{' '}
      {queueHref && (
        <a class="jqr-link" href={queueHref}>view queue →</a>
      )}
    </div>
  );
}
