import {
  Fragment,
  memo,
  useCallback,
  useDeferredValue,
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import type { ReactNode } from 'react';
import { api } from '../api/client';
import { num } from '../lib/format';
import { Banner, Button, Card, Note, PageHead } from '../components/ui';
import { Icon } from '../components/Icon';
import {
  DRAWER_SECTIONS,
  EMPTY_FILTERS,
  activeChips,
  durationTone,
  facets,
  formatDuration,
  isSectioned,
  matches,
  serviceTone,
  shape,
} from '../lib/logmodel';
import type { LogFilters, Shaped } from '../lib/logmodel';
import type { LogEvent, LogResponse } from '../api/types';

/* The live server log.
 *
 * Network delivery is cursor based: each server record crosses the wire once. Rendering is
 * virtualised, so retaining and filtering thousands of records does not mean mounting
 * thousands of rows — only those around the viewport exist in the DOM.
 *
 * What changed, and why: the table used to print a record's attribute bag as one string,
 * which is complete and unreadable. `lib/logmodel` turns a record into the four answers a
 * person is actually after — when, which part of the server, what happened, did it work —
 * and this file is only the table, the filters and the drawer over that. The drawer is
 * where the evidence lives now, rather than where the meaning was.
 *
 * Three properties are easy to give back and worth keeping:
 *
 *  - **Rows have two heights, not one.** Repetitive request traffic stays on one line,
 *    because density is the reason this page is worth watching; a failure or a real
 *    application event earns a second. That means the virtual window is driven by a prefix
 *    sum of row heights rather than by multiplication, computed once per filter change.
 *    Which height a row gets and what it prints are one decision, made once, in
 *    `lib/logmodel`'s `secondary` — the table renders that field and nothing else under
 *    the summary. They were two, and they disagreed: every authenticated request line
 *    carries the viewer and the television, so a context line was drawn on rows the height
 *    rule had already ruled out, centred inside a box too short for it and sliced top and
 *    bottom. A request's identity now sits at the end of its own line instead, which keeps
 *    both the density and the fact.
 *  - **Pause holds the view, not the connection.** Draining continues while paused and the
 *    arrivals are held in a buffer, so the cursor keeps up with the server's ring buffer
 *    and resuming is a flush rather than a stampede — the previous behaviour let the ring
 *    overwrite records the operator had paused specifically in order to read around.
 *  - **Nothing snaps.** The view follows the tail only while the operator is already at
 *    it; scrolling up hands them a `Jump to latest` instead. */

const RETAIN = 20_000;
const POLL_MS = 5_000;
/* Row geometry, and it is arithmetic rather than a pair of round numbers: a row is
 * absolutely positioned at a height this file decides, so anything the stylesheet draws
 * that these figures do not account for is text clipped by a rule nobody can see from the
 * CSS. Every first-line cell in `.logrow` is given exactly ROW_LINE, the secondary line
 * exactly ROW_SECOND, and the padding and hairline below are the same on both heights —
 * which is what makes the columns line up whether an event printed one line or two.
 * Changing any figure here means changing its twin in `styles.css`. */
const ROW_PAD = 7;
const ROW_LINE = 16;
const ROW_SECOND = 15;
const ROW_SECOND_GAP = 2;
const ROW_RULE = 1;
const ROW_COMPACT = ROW_PAD + ROW_LINE + ROW_PAD + ROW_RULE;
const ROW_TALL = ROW_COMPACT + ROW_SECOND_GAP + ROW_SECOND;
const DAY_HEIGHT = 26;
const HEADER_HEIGHT = 31;
const OVERSCAN = 10;

const LEVELS = [
  { value: 'TRACE', label: 'Everything' },
  { value: 'DEBUG', label: 'Debug+' },
  { value: 'INFO', label: 'Info+' },
  { value: 'WARN', label: 'Warnings+' },
  { value: 'ERROR', label: 'Errors only' },
];

const STATUS_BANDS = [
  { value: '', label: 'Any result' },
  { value: '2xx', label: 'Success (2xx)' },
  { value: '3xx', label: 'Redirect (3xx)' },
  { value: '4xx', label: 'Client error (4xx)' },
  { value: '5xx', label: 'Server error (5xx)' },
  { value: 'error', label: 'Failed (≥400)' },
];

const SLOWER = [
  { value: 0, label: 'Any duration' },
  { value: 100, label: 'Slower than 100 ms' },
  { value: 500, label: 'Slower than 500 ms' },
  { value: 1000, label: 'Slower than 1 s' },
  { value: 3000, label: 'Slower than 3 s' },
];

const readableKey = (key: string) => key.replace(/_/g, ' ');

/* ---------- one row ---------- */

/** A value in the table that is also a filter. Clicking a service, a level, a method or a
 *  status is by some way the fastest way into a subsystem, and it costs nothing to make
 *  the thing already printed be the control. */
function Facet({
  onPick,
  className,
  title,
  children,
  ...rest
}: {
  onPick: () => void;
  className: string;
  title: string;
  children: ReactNode;
} & Record<string, unknown>) {
  return (
    <button type="button" className={`logfacet ${className}`} title={title} onClick={onPick} {...rest}>
      {children}
    </button>
  );
}

const LogRow = memo(function LogRow({
  event,
  view,
  top,
  height,
  selected,
  onInspect,
  onFilter,
}: {
  event: LogEvent;
  view: Shaped;
  top: number;
  height: number;
  selected: boolean;
  onInspect: (sequence: number) => void;
  onFilter: (patch: Partial<LogFilters>) => void;
}) {
  const slow = view.durationMs !== null ? durationTone(view.durationMs) : null;
  return (
    <div
      className="logrow"
      data-level={view.level}
      data-selected={selected || undefined}
      style={{ transform: `translateY(${top}px)`, height: `${height}px` }}
    >
      <time className="logrow-time" title={event.occurredAt}>
        {view.time}
      </time>

      <Facet
        className="logrow-level"
        data-level={view.level}
        title={`Show ${view.level} and above`}
        onPick={() => onFilter({ level: view.level })}
      >
        {view.level}
      </Facet>

      <span className="logrow-place">
        <Facet
          className="logrow-service"
          data-tone={serviceTone(view.serviceKey)}
          title={`Filter to ${view.service}`}
          onPick={() => onFilter({ service: view.serviceKey, component: '' })}
        >
          {view.service}
        </Facet>
        <span className="logrow-sep" aria-hidden="true">
          ›
        </span>
        <Facet
          className="logrow-component"
          title={`Filter to ${view.component}`}
          onPick={() => onFilter({ component: view.component })}
        >
          {view.component}
        </Facet>
      </span>

      <button
        type="button"
        className="logrow-summary"
        title={[view.summary, view.trail, view.secondary].filter(Boolean).join(' — ')}
        onClick={() => onInspect(event.sequence)}
      >
        <span className="logrow-line">
          {view.action ? (
            <b className="logrow-action" data-method={view.method || undefined}>
              {view.action}
            </b>
          ) : null}
          <span className="logrow-text">{view.summary}</span>
          {view.trail ? <span className="logrow-trail">{view.trail}</span> : null}
        </span>
        {/* Printed if and only if the row was measured for it — see `secondary` in
            lib/logmodel. */}
        {view.secondary ? (
          <span className="logrow-second" data-tone={view.secondaryTone}>
            {view.secondaryTone === 'error' ? `↳ ${view.secondary}` : view.secondary}
          </span>
        ) : null}
      </button>

      <span className="logrow-result">
        {view.result ? (
          <Facet
            className="logrow-verdict"
            data-tone={view.result.tone}
            title={
              view.status !== null ? `Filter to ${view.status}` : `Filter to ${view.eventKey}`
            }
            onPick={() =>
              view.status !== null
                ? onFilter({ status: `${Math.floor(view.status / 100)}xx` })
                : onFilter({ event: view.eventKey })
            }
          >
            {view.result.label}
          </Facet>
        ) : null}
      </span>

      <span className="logrow-duration" data-tone={slow ?? undefined}>
        {view.durationMs !== null ? formatDuration(view.durationMs) : ''}
      </span>
    </div>
  );
});

/* ---------- the drawer ---------- */

function Drawer({
  event,
  view,
  onClose,
}: {
  event: LogEvent;
  view: Shaped;
  onClose: () => void;
}) {
  const [copied, setCopied] = useState(false);
  const leftovers = view.fields.filter(([key]) => !isSectioned(key) && key !== 'component');

  const copy = async () => {
    try {
      await navigator.clipboard.writeText(JSON.stringify(event, null, 2));
      setCopied(true);
      window.setTimeout(() => setCopied(false), 1600);
    } catch {
      setCopied(false);
    }
  };

  const sections = DRAWER_SECTIONS.map((section) => ({
    title: section.title,
    rows: section.keys
      .map((key) => [key, view.attributes[key]] as [string, unknown])
      .filter(([, value]) => value !== undefined && value !== null && String(value) !== ''),
  })).filter((section) => section.rows.length > 0);

  return (
    <section className="logdrawer" aria-label={`Log record ${event.sequence}`}>
      <header className="logdrawer-head">
        <div>
          <p className="logdrawer-place">
            <span className="logrow-service" data-tone={serviceTone(view.serviceKey)}>
              {view.service}
            </span>
            <span className="logrow-sep" aria-hidden="true">
              ›
            </span>
            {view.component}
          </p>
          <b>{view.summary}</b>
          {view.detail ? <p className="logdrawer-error">{view.detail}</p> : null}
        </div>
        <div className="logdrawer-actions">
          <Button size="sm" variant="quiet" onClick={copy} icon="download">
            {copied ? 'Copied' : 'Copy JSON'}
          </Button>
          <Button size="sm" variant="quiet" onClick={onClose} icon="close">
            Close
          </Button>
        </div>
      </header>

      <div className="logdrawer-grid">
        <div className="logdrawer-section">
          <h4>Overview</h4>
          <dl>
            <dt>Time</dt>
            <dd>
              {view.day} {view.time}
            </dd>
            <dt>Level</dt>
            <dd>{view.level}</dd>
            <dt>Service</dt>
            <dd>
              {view.service} › {view.component}
            </dd>
            <dt>Event</dt>
            <dd>{view.eventKey}</dd>
            {view.result ? (
              <>
                <dt>Result</dt>
                <dd>{view.result.label}</dd>
              </>
            ) : null}
            {view.durationMs !== null ? (
              <>
                <dt>Duration</dt>
                <dd>{formatDuration(view.durationMs)}</dd>
              </>
            ) : null}
            <dt>Record</dt>
            <dd>#{event.sequence}</dd>
          </dl>
        </div>

        {sections.map((section) => (
          <div className="logdrawer-section" key={section.title}>
            <h4>{section.title}</h4>
            <dl>
              {section.rows.map(([key, value]) => (
                <Fragment key={key}>
                  <dt>{readableKey(key)}</dt>
                  <dd>{String(value)}</dd>
                </Fragment>
              ))}
            </dl>
          </div>
        ))}

        {leftovers.length ? (
          <div className="logdrawer-section">
            <h4>Details</h4>
            <dl>
              {leftovers.map(([key, value]) => (
                <Fragment key={key}>
                  <dt>{readableKey(key)}</dt>
                  <dd>{String(value)}</dd>
                </Fragment>
              ))}
            </dl>
          </div>
        ) : null}
      </div>

      <details className="logdrawer-raw">
        <summary>Raw event</summary>
        <pre>{JSON.stringify(event, null, 2)}</pre>
      </details>
    </section>
  );
}

/* ---------- the page ---------- */

export function LogsPage() {
  const [records, setRecords] = useState<LogEvent[]>([]);
  const [dropped, setDropped] = useState(0);
  const [paused, setPaused] = useState(false);
  const [held, setHeld] = useState(0);
  /* A ?q= in the address seeds the text filter once, so a page that has diagnosed
     something can hand the operator the logs already narrowed to it — which is what the
     Integrations area's "open the logs" link does with a failing service's name. Seeded
     on the initial state rather than in an effect: applying it later would fight whatever
     the operator had already typed, and the whole point of a deep link is that it is where
     they arrive rather than something that happens to them. */
  const [filters, setFilters] = useState<LogFilters>(() => {
    const seed = new URLSearchParams(window.location.search).get('q') ?? '';
    return seed ? { ...EMPTY_FILTERS, text: seed } : EMPTY_FILTERS;
  });
  const [error, setError] = useState('');
  const [viewport, setViewport] = useState({ top: 0, height: 600 });
  const [atTail, setAtTail] = useState(true);
  const [selectedSequence, setSelectedSequence] = useState<number | null>(null);

  const deferredSearch = useDeferredValue(filters.text.trim().toLowerCase());
  const cursor = useRef(0);
  const fetching = useRef(false);
  const viewGeneration = useRef(0);
  const view = useRef<HTMLDivElement>(null);
  const pinned = useRef(true);
  const scrollFrame = useRef<number | undefined>(undefined);
  // Arrivals while paused. Held here rather than left on the server: the ring buffer is
  // finite, and a pause taken in order to read something is exactly when it must not be
  // overwritten underneath the operator.
  const holding = useRef<LogEvent[]>([]);
  // Read by `drain`, which is a timer callback rather than a render. Written in an effect
  // rather than during render: a render that concurrent React discards must not be able to
  // decide whether the next batch of arrivals is shown or held.
  const pausedRef = useRef(paused);
  useEffect(() => {
    pausedRef.current = paused;
  }, [paused]);

  const admit = useCallback((batch: LogEvent[]) => {
    setRecords((current) => {
      const next = current.concat(batch);
      return next.length > RETAIN ? next.slice(next.length - RETAIN) : next;
    });
  }, []);

  const drain = useCallback(async () => {
    if (fetching.current || document.hidden) return;
    fetching.current = true;
    const generation = viewGeneration.current;
    const batch: LogEvent[] = [];
    let droppedInDrain = 0;
    try {
      let pages = 0;
      let page: LogResponse;
      do {
        page = await api.get<LogResponse>(`/admin/api/events?after=${cursor.current}&limit=1000`);
        cursor.current = page.next || cursor.current;
        droppedInDrain += page.dropped || 0;
        batch.push(...(page.events ?? []));
        pages += 1;
      } while (page.hasMore && pages < 20);
      setError('');
    } catch (cause) {
      setError(cause instanceof Error ? cause.message : String(cause));
    } finally {
      // One React update for a complete catch-up prevents the initial drain from redrawing
      // the page once per network page.
      if (batch.length > 0 && generation === viewGeneration.current) {
        if (pausedRef.current) {
          holding.current = holding.current.concat(batch);
          if (holding.current.length > RETAIN) {
            holding.current = holding.current.slice(holding.current.length - RETAIN);
          }
          setHeld(holding.current.length);
        } else {
          admit(batch);
        }
      }
      if (droppedInDrain > 0 && generation === viewGeneration.current) {
        setDropped((current) => current + droppedInDrain);
      }
      fetching.current = false;
    }
  }, [admit]);

  useEffect(() => {
    let timer: number | undefined;
    const schedule = () => {
      window.clearInterval(timer);
      timer = document.hidden ? undefined : window.setInterval(() => void drain(), POLL_MS);
    };
    const visibilityChanged = () => {
      schedule();
      if (!document.hidden) void drain();
    };
    void drain();
    schedule();
    document.addEventListener('visibilitychange', visibilityChanged);
    return () => {
      window.clearInterval(timer);
      document.removeEventListener('visibilitychange', visibilityChanged);
    };
  }, [drain]);

  // Both halves set the ref straight away as well as the state. A drain landing between
  // the click and the commit would otherwise file its batch on the wrong side of the
  // pause — held records with the button already reading "Pause", which nothing would
  // ever flush.
  const pause = useCallback(() => {
    pausedRef.current = true;
    setPaused(true);
  }, []);

  const resume = useCallback(() => {
    pausedRef.current = false;
    const waiting = holding.current;
    holding.current = [];
    setHeld(0);
    setPaused(false);
    if (waiting.length) admit(waiting);
  }, [admit]);

  const patch = useCallback((next: Partial<LogFilters>) => {
    setFilters((current) => ({ ...current, ...next }));
  }, []);

  const filtered = useMemo(
    () => records.filter((event) => matches(event, filters, deferredSearch)),
    [records, filters, deferredSearch],
  );

  /* Row geometry. Two heights and a day divider mean the window cannot be found by
   * dividing a scroll offset, so heights are accumulated once per filter change and the
   * first visible row is found by binary search. Twenty thousand records is one pass over
   * a typed array — cheaper than the render it replaces. */
  const layout = useMemo(() => {
    const tops = new Float64Array(filtered.length + 1);
    const heights = new Uint8Array(filtered.length);
    const divider = new Uint8Array(filtered.length);
    let y = 0;
    let day = '';
    for (let index = 0; index < filtered.length; index += 1) {
      const record = filtered[index];
      if (!record) continue;
      const shaped = shape(record);
      if (shaped.dayKey !== day) {
        divider[index] = 1;
        day = shaped.dayKey;
        y += DAY_HEIGHT;
      }
      const height = shaped.tall ? ROW_TALL : ROW_COMPACT;
      tops[index] = y;
      heights[index] = height;
      y += height;
    }
    tops[filtered.length] = y;
    return { tops, heights, divider, total: y };
  }, [filtered]);

  const bodyTop = Math.max(0, viewport.top - HEADER_HEIGHT);
  const first = useMemo(() => {
    let low = 0;
    let high = filtered.length;
    while (low < high) {
      const middle = (low + high) >> 1;
      if ((layout.tops[middle] ?? 0) + (layout.heights[middle] ?? 0) <= bodyTop) low = middle + 1;
      else high = middle;
    }
    return Math.max(0, low - OVERSCAN);
  }, [layout, bodyTop, filtered.length]);

  const last = useMemo(() => {
    const limit = bodyTop + viewport.height;
    let index = first;
    while (index < filtered.length && (layout.tops[index] ?? 0) < limit) index += 1;
    return Math.min(filtered.length, index + OVERSCAN);
  }, [layout, bodyTop, viewport.height, first, filtered.length]);

  const windowed = useMemo(() => {
    const rows: { event: LogEvent; view: Shaped; index: number }[] = [];
    for (let index = first; index < last; index += 1) {
      const record = filtered[index];
      if (record) rows.push({ event: record, view: shape(record), index });
    }
    return rows;
  }, [filtered, first, last]);

  const lastSequence = filtered.at(-1)?.sequence ?? 0;
  const selected = useMemo(
    () => records.find((event) => event.sequence === selectedSequence),
    [records, selectedSequence],
  );

  const scrollToTail = useCallback(() => {
    const node = view.current;
    if (!node) return;
    pinned.current = true;
    node.scrollTop = node.scrollHeight;
    setAtTail(true);
    setViewport({ top: node.scrollTop, height: node.clientHeight });
  }, []);

  useLayoutEffect(() => {
    const node = view.current;
    if (!node || !pinned.current) return;
    node.scrollTop = node.scrollHeight;
    setViewport({ top: node.scrollTop, height: node.clientHeight });
  }, [lastSequence, layout.total]);

  useEffect(() => () => window.cancelAnimationFrame(scrollFrame.current ?? 0), []);

  const onScroll = () => {
    const node = view.current;
    if (!node) return;
    const tail = node.scrollHeight - node.scrollTop - node.clientHeight < ROW_TALL;
    pinned.current = tail;
    setAtTail(tail);
    window.cancelAnimationFrame(scrollFrame.current ?? 0);
    scrollFrame.current = window.requestAnimationFrame(() => {
      setViewport({ top: node.scrollTop, height: node.clientHeight });
    });
  };

  const exportJson = () => {
    const blob = new Blob([JSON.stringify(filtered, null, 2)], { type: 'application/json' });
    const link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = `memby-events-${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
    link.click();
    window.setTimeout(() => URL.revokeObjectURL(link.href), 1000);
  };

  const available = useMemo(() => facets(records), [records]);
  const chips = activeChips(filters, available.services);

  return (
    <>
      <PageHead title="Server logs" intro="Structured gateway events as they happen." />
      <Banner message={error} />

      <Card>
        <div className="logbar">
          <div className="logbar-filters">
            <select
              aria-label="Service"
              value={filters.service}
              onChange={(event) => patch({ service: event.target.value, component: '' })}
            >
              <option value="">All services</option>
              {available.services.map((service) => (
                <option key={service.key} value={service.key}>
                  {service.label}
                </option>
              ))}
            </select>
            <select
              aria-label="Component"
              value={filters.component}
              onChange={(event) => patch({ component: event.target.value })}
            >
              <option value="">All components</option>
              {available.components.map((component) => (
                <option key={component} value={component}>
                  {component}
                </option>
              ))}
            </select>
            <select
              aria-label="Level"
              value={filters.level}
              onChange={(event) => patch({ level: event.target.value })}
            >
              {LEVELS.map((level) => (
                <option key={level.value} value={level.value}>
                  {level.label}
                </option>
              ))}
            </select>
            <select
              aria-label="Event"
              value={filters.event}
              onChange={(event) => patch({ event: event.target.value })}
            >
              <option value="">All events</option>
              {available.events.map((name) => (
                <option key={name} value={name}>
                  {name}
                </option>
              ))}
            </select>
            <select
              aria-label="Result"
              value={filters.status}
              onChange={(event) => patch({ status: event.target.value })}
            >
              {STATUS_BANDS.map((band) => (
                <option key={band.value} value={band.value}>
                  {band.label}
                </option>
              ))}
            </select>
            <select
              aria-label="Method"
              value={filters.method}
              onChange={(event) => patch({ method: event.target.value })}
            >
              <option value="">Any method</option>
              {available.methods.map((method) => (
                <option key={method} value={method}>
                  {method}
                </option>
              ))}
            </select>
            <select
              aria-label="Duration"
              value={String(filters.slower)}
              onChange={(event) => patch({ slower: Number(event.target.value) })}
            >
              {SLOWER.map((option) => (
                <option key={option.value} value={String(option.value)}>
                  {option.label}
                </option>
              ))}
            </select>
            <label className="logsearch">
              <Icon name="search" />
              <input
                type="search"
                value={filters.text}
                aria-label="Search logs"
                placeholder="Search person, title, service, component, path, request ID…"
                onChange={(event) => patch({ text: event.target.value })}
              />
            </label>
          </div>

          <div className="logbar-actions">
            <Button
              size="sm"
              variant="quiet"
              onClick={() => (paused ? resume() : pause())}
              icon={paused ? 'play' : 'clock'}
            >
              {paused ? (held ? `Resume (${num(held)})` : 'Resume') : 'Pause'}
            </Button>
            <Button
              size="sm"
              variant="quiet"
              onClick={() => {
                // A page already in flight may finish after this click. Advancing the
                // generation makes that batch part of the cleared past, not a flash of old
                // lines reappearing after the view was emptied.
                viewGeneration.current += 1;
                holding.current = [];
                setHeld(0);
                setRecords([]);
                setDropped(0);
                setSelectedSequence(null);
              }}
            >
              Clear view
            </Button>
            <Button
              size="sm"
              variant="quiet"
              onClick={exportJson}
              icon="download"
              title="Export the rows matching the current filters, as delivered by the gateway"
            >
              Export JSON
            </Button>
          </div>
        </div>

        {chips.length ? (
          <div className="logchips">
            {chips.map((chip) => (
              <button
                key={chip.key}
                type="button"
                className="logchip"
                onClick={() => patch({ [chip.key]: EMPTY_FILTERS[chip.key] } as Partial<LogFilters>)}
              >
                {chip.label}
                <Icon name="close" />
              </button>
            ))}
            <button type="button" className="logchip logchip-clear" onClick={() => setFilters(EMPTY_FILTERS)}>
              Clear all
            </button>
          </div>
        ) : null}

        <div className="logshell">
          <div className="logview" ref={view} onScroll={onScroll} role="log" aria-label="Server events">
            <div className="loghead" aria-hidden="true">
              <span>Time</span>
              <span>Level</span>
              <span>Service</span>
              <span>Event</span>
              <span>Result</span>
              <span>Duration</span>
            </div>
            {filtered.length === 0 ? (
              <p className="empty">
                {records.length === 0 ? 'Waiting for server events…' : 'No events match these filters.'}
              </p>
            ) : (
              <div className="logbody" style={{ height: `${layout.total}px` }}>
                {windowed.map(({ event, view: shaped, index }) => (
                  <Fragment key={event.sequence}>
                    {layout.divider[index] ? (
                      <div
                        className="logday"
                        style={{ transform: `translateY(${(layout.tops[index] ?? 0) - DAY_HEIGHT}px)` }}
                      >
                        <span>{shaped.day}</span>
                      </div>
                    ) : null}
                    <LogRow
                      event={event}
                      view={shaped}
                      top={layout.tops[index] ?? 0}
                      height={layout.heights[index] ?? ROW_COMPACT}
                      selected={event.sequence === selectedSequence}
                      onInspect={setSelectedSequence}
                      onFilter={patch}
                    />
                  </Fragment>
                ))}
              </div>
            )}
          </div>

          {!atTail && filtered.length > 0 ? (
            <button type="button" className="logtail" onClick={scrollToTail}>
              <Icon name="caret" />
              Jump to latest
            </button>
          ) : null}
        </div>

        <p className="hint">
          {num(records.length)} retained · {num(filtered.length)} matching
          {filtered.length ? ` · ${num(windowed.length)} rows mounted` : ''}
          {dropped ? ` · ${num(dropped)} overwritten before delivery` : ''}
          {paused ? ` · paused${held ? `, ${num(held)} held` : ''}` : ''}
        </p>

        {selected ? (
          <Drawer event={selected} view={shape(selected)} onClose={() => setSelectedSequence(null)} />
        ) : records.length ? (
          <Note>Select a row to see the full record — request, context, diagnostics and raw event.</Note>
        ) : null}
      </Card>
    </>
  );
}
