import { memo, type ReactNode } from 'react';

import { cn } from '@/lib/utils';

import { channelLabel, enumLabel, hours, kycEntries, money, wholeUnits } from './format';
import { bestAlternate } from './grade';
import type { Lookup, Reveal, Screen, Transaction, TriageState } from './types';

/**
 * The analyst's screen. One component for all three jobs — the visitor
 * playing, the recorded replay, and the timeline thumbnail — because three
 * near-identical boards is how they drift.
 *
 * It is an instrument, not a document: dense, every id and number in mono,
 * every sentence in sans. It draws no outer chrome — the shell's card supplies
 * the border, ground and padding — and nothing inside it is a link or a
 * button; the actions live in the controls beside it.
 *
 * Two things this board must do that a game board does not:
 *
 *   · Say, on the board itself, that the data is synthetic. Not in a footnote.
 *     The label is the first row of every render, the thumbnail included.
 *   · Show the answer only once the episode is over. `state.reveal` is null
 *     until then, so the surface cannot leak the label even by accident.
 *
 * Nothing here interprets the data for the visitor. A counterparty shared with
 * nine other alerted accounts is rendered as the number nine, the way the
 * model saw it; whether nine is a lot is the analyst's call to make.
 */

const MAX_TURNS = 8;

/* ---------------------------------------------------------------- pieces */

/**
 * Warning ink on the warning ground: 6.08 light / 9.12 dark (index.css). The
 * dashed edge is the one place on the site a border is dashed, so the shape
 * itself says "a note about the data".
 */
function SyntheticLabel({ compact }: { compact?: boolean }) {
  return (
    <p
      className={cn(
        'rounded-tile border border-dashed border-warning bg-warning-subtle font-mono font-medium uppercase tracking-label text-warning',
        compact ? 'px-1 py-0.5 text-[7px] leading-tight' : 'px-2 py-1 text-label',
      )}
    >
      Synthetic data — no real customer, account or transaction
    </p>
  );
}

/** An identifier. `fg` for something the analyst can cite, `muted` for a reference to one. */
function Id({ children, tone = 'muted' }: { children: ReactNode; tone?: 'muted' | 'fg' }) {
  return <code className={cn('font-mono', tone === 'muted' ? 'text-muted' : 'text-fg')}>{children}</code>;
}

/** The mono 11 uppercase label, the same style as the shell's chrome. */
function Label({ children, className }: { children: ReactNode; className?: string }) {
  return <span className={cn('font-mono text-label uppercase text-muted', className)}>{children}</span>;
}

/** A 4px outline chip for a state word. Never colour alone: the word is the signal. */
function Chip({ children, className, title }: { children: ReactNode; className?: string; title?: string }) {
  return (
    <span
      className={cn(
        'inline-flex items-center gap-1 whitespace-nowrap rounded-tile border border-border-strong px-1.5 py-px font-mono text-label uppercase',
        className,
      )}
      title={title}
    >
      {children}
    </span>
  );
}

function Block({ title, aside, children }: { title: string; aside?: ReactNode; children: ReactNode }) {
  return (
    <section className="min-w-0 rounded-control border border-border bg-surface p-3">
      <header className="mb-2 flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">
        <h4 className="font-mono text-label uppercase text-muted">{title}</h4>
        {aside ? <div className="min-w-0 font-mono text-caption text-muted">{aside}</div> : null}
      </header>
      {children}
    </section>
  );
}

/**
 * `contain: inline-size` is load-bearing. The board lives inside a
 * shrink-to-fit card, so a table's min-content width would otherwise become
 * the board's minimum width and push a 390px phone into horizontal scroll.
 * Containment makes the wrapper contribute nothing and scroll instead.
 */
function Scroll({ children }: { children: ReactNode }) {
  return <div className="overflow-x-auto [contain:inline-size]">{children}</div>;
}

/** Table styling shared by every table on the screen: mono 12, heads mono 11 on surface-3. */
const TABLE = 'w-full border-collapse font-mono text-caption';
const TH = 'bg-surface-3 px-2 py-1 text-left font-mono text-label font-medium uppercase text-muted first:rounded-l-tile last:rounded-r-tile';
const TD = 'px-2 py-1 align-baseline';
const NUM = 'whitespace-nowrap text-right';

function TransactionTable({ rows, currency }: { rows: Transaction[]; currency: string }) {
  if (rows.length === 0) return <p className="font-sans text-small text-muted">No transactions.</p>;
  return (
    <Scroll>
      <table className={cn(TABLE, 'min-w-[26rem]')}>
        <thead>
          <tr>
            <th className={TH}>id</th>
            <th className={cn(TH, 'text-right')}>date</th>
            <th className={TH}>channel</th>
            <th className={cn(TH, 'text-right')}>amount</th>
            <th className={TH}>where</th>
          </tr>
        </thead>
        <tbody>
          {rows.map((t) => (
            <tr key={t.id} className="border-t border-border">
              <td className={TD}>
                <Id tone="fg">{t.id}</Id>
              </td>
              <td className={cn(TD, NUM)}>{t.date}</td>
              <td className={cn(TD, 'whitespace-nowrap font-sans text-small')}>{channelLabel(t.channel)}</td>
              <td className={cn(TD, NUM, 'text-fg')}>{money(t.amount, currency)}</td>
              <td className={cn(TD, 'whitespace-nowrap')}>
                {t.branch ? (
                  <span className="font-sans text-small">branch {t.branch}</span>
                ) : t.cp ? (
                  <Id>{t.cp}</Id>
                ) : (
                  <span className="text-muted">—</span>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </Scroll>
  );
}

function AlertBlock({ screen }: { screen: Screen }) {
  const a = screen.alert;
  return (
    <Block title="Alert" aside={<span>fired {a.fired}</span>}>
      <p className="font-sans text-small text-fg">
        <Id tone="fg">{a.id}</Id>
        <span className="mx-1.5 text-muted">·</span>
        <span className="font-mono font-semibold">{a.rule}</span>
        <span className="mx-1.5 text-muted">—</span>
        <span className="text-fg-2">{a.rule_text}</span>
      </p>
      <p className="mb-1 mt-2">
        <Label>Triggering transactions</Label>
      </p>
      <TransactionTable rows={screen.triggering_transactions} currency={screen.currency} />
    </Block>
  );
}

function kycValue(key: string, value: unknown, currency: string): string {
  if (typeof value === 'boolean') return value ? 'yes' : 'no';
  if (typeof value === 'number') {
    return key === 'stated_income' || key === 'expected_monthly_cash' ? money(value, currency) : String(value);
  }
  return String(value ?? '');
}

function KycBlock({ screen }: { screen: Screen }) {
  const c = screen.customer;
  return (
    <Block title="KYC file" aside={<Id>{c.id}</Id>}>
      <p className="mb-2 font-sans text-ui font-medium text-fg">{c.name}</p>
      <dl className="grid grid-cols-1 gap-x-4 gap-y-1.5 sm:grid-cols-2">
        {kycEntries(c as unknown as Record<string, unknown>).map(([key, value]) => (
          <div key={key} className="flex min-w-0 flex-col border-t border-border pt-1">
            <dt className="font-mono text-caption text-muted">kyc.{key}</dt>
            <dd className="break-words font-sans text-small text-fg">{kycValue(key, value, screen.currency)}</dd>
          </div>
        ))}
      </dl>
    </Block>
  );
}

function SummaryBlock({ screen }: { screen: Screen }) {
  const cols: [Exclude<keyof Screen['summary'][number], 'month'>, string][] = [
    ['cash_in', 'cash in'],
    ['cash_out', 'cash out'],
    ['wires_in', 'wires in'],
    ['wires_out', 'wires out'],
    ['ach_in', 'ACH in'],
    ['ach_out', 'ACH out'],
  ];
  return (
    <Block
      title="Twelve-month summary"
      aside={
        <span>
          whole {screen.currency}
          {screen.screen_order === 'kyc_first' ? ' · newest first' : ''}
        </span>
      }
    >
      <Scroll>
        <table className={cn(TABLE, 'min-w-[30rem]')}>
          <thead>
            <tr>
              <th className={TH}>month</th>
              {cols.map(([key, label]) => (
                <th key={key} className={cn(TH, 'text-right')}>
                  {label}
                </th>
              ))}
              <th className={cn(TH, 'text-right')}>txns</th>
            </tr>
          </thead>
          <tbody>
            {screen.summary.map((row) => (
              <tr key={row.month} className="border-t border-border">
                <td className={cn(TD, 'whitespace-nowrap text-fg')}>{row.month}</td>
                {cols.map(([key]) => (
                  <td key={key} className={cn(TD, NUM)}>
                    {wholeUnits(row[key])}
                  </td>
                ))}
                <td className={cn(TD, NUM, 'text-muted')}>{row.count}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </Scroll>
    </Block>
  );
}

function CounterpartyBlock({ screen }: { screen: Screen }) {
  return (
    <Block title="Counterparties on screen" aside="top by twelve-month volume">
      <Scroll>
        <table className={cn(TABLE, 'min-w-[20rem]')}>
          <thead>
            <tr>
              <th className={TH}>id</th>
              <th className={TH}>direction</th>
              <th className={cn(TH, 'text-right')}>txns</th>
              <th className={cn(TH, 'text-right')}>total</th>
            </tr>
          </thead>
          <tbody>
            {screen.screen_counterparties.map((cp) => (
              <tr key={cp.id} className="border-t border-border">
                <td className={TD}>
                  <Id tone="fg">{cp.id}</Id>
                </td>
                <td className={cn(TD, 'font-sans text-small text-muted')}>{cp.direction === 'in' ? 'inbound' : 'outbound'}</td>
                <td className={cn(TD, NUM)}>{cp.count}</td>
                <td className={cn(TD, NUM, 'text-fg')}>{money(cp.total, screen.currency)}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </Scroll>
      <p className="mt-2 font-sans text-small text-muted">
        Reporting threshold in this jurisdiction:{' '}
        <span className="font-mono text-fg">{money(screen.threshold, screen.currency)}</span>
      </p>
    </Block>
  );
}

function LookupCard({ lookup, currency }: { lookup: Lookup; currency: string }) {
  switch (lookup.what) {
    case 'history':
      return (
        <Block
          title={`History ${lookup.result.month}`}
          aside={<span>{lookup.result.transactions.length} transactions</span>}
        >
          <TransactionTable rows={lookup.result.transactions} currency={currency} />
        </Block>
      );
    case 'counterparty': {
      const r = lookup.result;
      return (
        <Block title={`Counterparty ${r.id}`} aside={<span>first seen {r.first_seen}</span>}>
          <p className="font-sans text-ui font-medium text-fg">{r.name}</p>
          <p className="font-sans text-small text-muted">
            {r.type.replace(/_/g, ' ')} · {r.jurisdiction}
          </p>
          <p className="mt-1.5 font-sans text-small text-fg-2">
            Other alerted accounts sharing this counterparty in twelve months:{' '}
            <span className="font-mono font-semibold text-fg">{r.other_alerted_accounts}</span>
          </p>
          <p className="mt-1.5 font-sans text-small text-muted">
            transactions:{' '}
            {r.transactions.map((id) => (
              <span key={id} className="mr-1.5">
                <Id>{id}</Id>
              </span>
            ))}
          </p>
        </Block>
      );
    }
    case 'prior_alerts':
      return (
        <Block title="Prior alerts" aside={<span>{lookup.result.alerts.length} on file</span>}>
          {lookup.result.alerts.length === 0 ? (
            <p className="font-sans text-small text-muted">None on file.</p>
          ) : (
            <ul className="divide-y divide-border">
              {lookup.result.alerts.map((p) => (
                <li key={p.id} className="py-1.5 first:pt-0 last:pb-0">
                  <div className="flex flex-wrap items-baseline gap-x-2 font-mono text-caption">
                    <Id tone="fg">{p.id}</Id>
                    <span className="font-semibold text-fg">{p.rule}</span>
                    <span className="text-muted">fired {p.fired}</span>
                    <span className="font-sans text-small text-fg-2">
                      {p.disposition} after <span className="font-mono">{p.elapsed_days}</span>{' '}
                      {p.elapsed_days === 1 ? 'day' : 'days'} by {p.analyst}
                    </span>
                  </div>
                  <p className="mt-0.5 font-sans text-small text-fg-2">{p.rationale}</p>
                </li>
              ))}
            </ul>
          )}
        </Block>
      );
    case 'documents':
      return (
        <Block title="Documents on file" aside={<span>{lookup.result.documents.length}</span>}>
          <ul className="divide-y divide-border">
            {lookup.result.documents.map((d) => (
              <li key={d.id} className="flex flex-col gap-0.5 py-1.5 first:pt-0 last:pb-0 sm:flex-row sm:gap-3">
                <span className="shrink-0 font-mono text-caption">
                  <Id tone="fg">{d.id}</Id>{' '}
                  <span className="text-muted">[{d.kind.replace(/_/g, ' ')}]</span>
                </span>
                <span className="font-sans text-small text-fg-2">{d.description}</span>
              </li>
            ))}
          </ul>
        </Block>
      );
  }
}

/**
 * A cited id. Three states, each with a word for screen readers and a mark
 * for everyone else: planted (✓ on the green ground), not planted (struck
 * through), and unknown (a plain strong-edged chip — the episode is not
 * over, or the disposition was not scored).
 */
function CiteChip({ id, hit }: { id: string; hit?: boolean | null }) {
  return (
    <span
      className={cn(
        'inline-flex items-center gap-1 rounded-tile border px-1.5 py-0.5 font-mono text-label tracking-normal',
        hit === true && 'border-positive bg-accent-subtle text-fg',
        hit === false && 'border-border text-muted line-through',
        (hit === null || hit === undefined) && 'border-border-strong text-fg',
      )}
    >
      {hit === true ? <span aria-hidden="true">✓</span> : null}
      {id}
      {hit === true ? <span className="sr-only">, planted</span> : null}
      {hit === false ? <span className="sr-only">, not planted</span> : null}
    </span>
  );
}

function NeverScored() {
  return <Chip className="text-muted">displayed, never scored</Chip>;
}

function DispositionCard({ state }: { state: TriageState }) {
  const d = state.disposition;
  if (!d) return null;
  const planted = state.reveal ? bestAlternate(d.cites, state.reveal.planted) : null;
  const scored = state.reveal !== null && state.outcome === 'solved';
  const prose = (d.action === 'close' ? d.note : d.narrative).trim();
  return (
    <Block
      title={d.action === 'close' ? 'Closed' : 'Escalated'}
      aside={<span className="text-fg">{d.action === 'close' ? enumLabel(d.reason) : d.typology}</span>}
    >
      <p className="mb-1">
        <Label>Cites</Label>
      </p>
      <div className="flex flex-wrap gap-1">
        {d.cites.map((id) => (
          <CiteChip key={id} id={id} hit={planted && scored ? planted.includes(id) : null} />
        ))}
      </div>
      {prose ? (
        <div className="mt-2.5">
          <p className="mb-1 flex flex-wrap items-center gap-2">
            <Label>{d.action === 'close' ? 'Note' : 'Narrative'}</Label>
            <NeverScored />
          </p>
          <blockquote className="border-l-2 border-border-strong pl-2.5 font-sans text-small text-fg-2">{prose}</blockquote>
        </div>
      ) : null}
    </Block>
  );
}

type Tone = 'positive' | 'danger' | 'warning';

function verdictLine(state: TriageState, reveal: Reveal): { text: string; tone: Tone } {
  if (state.status === 'aborted') {
    return {
      text: 'No disposition in eight replies. The alert stays in the queue and the episode scores 0.000 on every component.',
      tone: 'danger',
    };
  }
  if (state.outcome === 'solved') {
    return {
      text:
        reveal.label === 'benign'
          ? 'Right call: the alert was benign and it was closed.'
          : 'Right call: the alert was suspicious and it was escalated.',
      tone: 'positive',
    };
  }
  if (reveal.label === 'suspicious') {
    return {
      text: 'Missed: a suspicious alert was closed. The counterweight and both objectives are zero — the episode scores 0.000.',
      tone: 'danger',
    };
  }
  return {
    text: 'A clean customer was escalated. Nothing slipped, but a report was filed on them: the counterweight pays half and the two objectives pay nothing.',
    tone: 'warning',
  };
}

/**
 * Verdict ink sits on surface-2, where all three status colours clear 4.5
 * in both themes (danger is 4.70 dark — surface-2 is the darkest ground it is
 * allowed on, see index.css). The sentence begins with its word.
 */
const TONE_TEXT: Record<Tone, string> = {
  positive: 'text-positive',
  danger: 'text-danger',
  warning: 'text-warning',
};

function RevealCard({ state }: { state: TriageState }) {
  const reveal = state.reveal;
  if (!reveal) return null;
  const cites = state.disposition?.cites ?? [];
  const best = bestAlternate(cites, reveal.planted);
  const verdict = verdictLine(state, reveal);
  return (
    <Block
      title="What the generator planted"
      aside={
        <span>
          {reveal.template}
          {reveal.overlay ? ` + ${reveal.overlay.replace(/_/g, ' ')}` : ''}
        </span>
      }
    >
      <p className="flex flex-wrap items-baseline gap-x-2 gap-y-1 font-sans text-small">
        <span className={cn('font-mono font-semibold uppercase', reveal.label === 'benign' ? 'text-positive' : 'text-danger')}>
          {reveal.label}
        </span>
        {reveal.typology ? <span className="font-mono text-caption text-fg">{reveal.typology}</span> : null}
        {reveal.tier === 'hidden' ? (
          <span className="text-muted">hidden tier: the screen is a benign case; the signal was only in a lookup</span>
        ) : null}
      </p>
      <p
        className={cn('mt-2 rounded-control bg-surface-2 px-2.5 py-2 font-sans text-small font-medium', TONE_TEXT[verdict.tone])}
        role="status"
      >
        {verdict.text}
      </p>
      <p className="mb-1 mt-2.5">
        <Label>The facts that decide it{reveal.planted.length > 1 ? ' (best of the alternates)' : ''}</Label>
      </p>
      <div className="flex flex-wrap gap-1">
        {best.map((id) => (
          <CiteChip key={id} id={id} hit={cites.includes(id) ? true : null} />
        ))}
      </div>
      <p className="mt-2.5 flex flex-wrap gap-x-3 gap-y-1 font-mono text-caption text-muted">
        <span>
          <Label>Hours spent</Label> <span className="text-fg">{hours(state.minutesSpent)}</span>
        </span>
        {reveal.referenceMinutes !== null ? (
          <span>
            <Label>Cheapest shipped analyst that got it right</Label>{' '}
            <span className="text-fg">{hours(reveal.referenceMinutes)}</span>
          </span>
        ) : null}
      </p>
    </Block>
  );
}

/**
 * The meter: what the episode has cost so far, and where it stands. The hour
 * count climbs with every lookup and the eight reply pips fill one per turn,
 * so the budget is visible without reading a number — and the numbers are
 * there for anyone who does.
 */
const STATUS_WORD: Record<TriageState['status'], string> = {
  playing: 'in the queue',
  closed: 'closed',
  escalated: 'escalated',
  aborted: 'left in the queue',
};
const STATUS_TONE: Record<TriageState['status'], string> = {
  playing: 'text-muted',
  closed: 'text-positive',
  escalated: 'text-warning',
  aborted: 'text-danger',
};

function Meter({ state }: { state: TriageState }) {
  return (
    <div className="flex flex-wrap items-center gap-x-4 gap-y-1.5 font-mono text-caption">
      <span className="flex items-baseline gap-1.5">
        <Label>Hours</Label>
        <span className="text-mono-data font-semibold text-fg">{hours(state.minutesSpent)}</span>
      </span>
      <span className="flex items-center gap-1.5">
        <Label>Replies</Label>
        <span className="text-mono-data font-semibold text-fg">
          {state.turnsSpent}
          <span className="font-normal text-muted"> of {MAX_TURNS}</span>
        </span>
        <span className="inline-flex items-center gap-0.5" aria-hidden="true">
          {Array.from({ length: MAX_TURNS }, (_, i) => (
            <span
              key={i}
              className={cn(
                'h-2.5 w-1.5 rounded-[1px] border',
                i < state.turnsSpent ? 'border-fg bg-fg' : 'border-border-strong bg-transparent',
              )}
            />
          ))}
        </span>
      </span>
      {state.rejected > 0 ? (
        <span className="flex items-baseline gap-1.5">
          <Label className="text-warning">Refused</Label>
          <span className="text-mono-data font-semibold text-warning">{state.rejected}</span>
        </span>
      ) : null}
      <Chip className={STATUS_TONE[state.status]}>{STATUS_WORD[state.status]}</Chip>
      {state.screen.held_out ? (
        <Chip
          className="text-fg"
          title="fnv1a(seed) % 8 == 7: second currency, renamed rules, KYC before the alert"
        >
          held-out seed
        </Chip>
      ) : null}
    </div>
  );
}

/* --------------------------------------------------------------- compact */

/**
 * The thumbnail: what the timeline chip and the gallery card show. A whole
 * screen at 7.5rem is noise, so this is the shape of the run instead — the
 * rule, the twelve months of cash as bars, and what has happened so far.
 */
function CompactBoard({ state }: { state: TriageState }) {
  const rows = state.screen.summary;
  const peak = Math.max(1, ...rows.map((r) => r.cash_in));
  const d = state.disposition;
  const label =
    `${state.screen.alert.rule}, ${hours(state.minutesSpent)} hours, ${state.lookups.length} lookups` +
    (d ? `, ${d.action === 'close' ? 'closed' : 'escalated'}` : state.status === 'aborted' ? ', aborted' : '');
  const CHIP = 'rounded-[2px] border px-0.5 leading-[1.3]';
  return (
    <div className="w-[7.5rem] font-mono text-[9px] leading-tight" role="img" aria-label={label}>
      <SyntheticLabel compact />
      <div className="mt-1 flex items-baseline justify-between gap-1">
        <span className="truncate font-semibold text-fg">{state.screen.alert.rule}</span>
        <span className="shrink-0 text-muted">{hours(state.minutesSpent)} h</span>
      </div>
      <div className="mt-1 flex h-4 items-end gap-px" aria-hidden="true">
        {rows.map((r) => (
          <span
            key={r.month}
            className="flex-1 rounded-[1px] bg-border-strong"
            style={{ height: `${Math.max(6, Math.round((r.cash_in / peak) * 100))}%` }}
          />
        ))}
      </div>
      <div className="mt-1 flex flex-wrap gap-0.5">
        {state.lookups.map((l, i) => (
          <span key={i} className={cn(CHIP, 'border-border text-muted')}>
            {l.what === 'history' ? l.key.slice(5) : l.what === 'counterparty' ? l.key : l.what === 'prior_alerts' ? 'priors' : 'docs'}
          </span>
        ))}
        {state.rejected > 0 ? (
          <span className={cn(CHIP, 'border-warning text-warning')}>!{state.rejected}</span>
        ) : null}
        {d ? (
          <span
            className={cn(
              CHIP,
              'font-semibold uppercase',
              d.action === 'close' ? 'border-positive text-positive' : 'border-warning text-warning',
            )}
          >
            {d.action === 'close' ? 'close' : 'escalate'}
          </span>
        ) : state.status === 'aborted' ? (
          <span className={cn(CHIP, 'border-danger font-semibold uppercase text-danger')}>aborted</span>
        ) : null}
      </div>
    </div>
  );
}

/* ----------------------------------------------------------------- board */

export const Board = memo(function Board({ state, compact }: { state: TriageState; compact?: boolean }) {
  if (compact) return <CompactBoard state={state} />;

  const { screen } = state;
  const blocks =
    screen.screen_order === 'kyc_first'
      ? [<KycBlock key="kyc" screen={screen} />, <AlertBlock key="alert" screen={screen} />]
      : [<AlertBlock key="alert" screen={screen} />, <KycBlock key="kyc" screen={screen} />];

  return (
    <div
      // An explicit width, capped by the VIEWPORT. The board sits in a
      // shrink-to-fit card on every tab, so `w-full` resolves to nothing and
      // `max-w-full` resolves against a parent that sized itself to this
      // board — measured: 640px on a 390px phone, 304px of page overflow.
      // 4.5rem is the phone gutters (2 × 1.25rem) plus the shell card's p-3
      // (2 × 0.75rem) with half a rem for its borders: the shell's padding
      // and this constant are coupled and move together.
      // `max-w-full` is for the one place the parent IS definite: a blind
      // comparison's grid cell, which is narrower than 40rem at 1280px and
      // measured 31px of page overflow without it.
      className="w-[min(40rem,calc(100vw-4.5rem))] min-w-0 max-w-full space-y-3 font-sans text-small text-fg"
      role="group"
      aria-label={`Alert ${screen.alert.id} on customer ${screen.customer.id}. ${state.lookups.length} lookups made, ${hours(state.minutesSpent)} hours spent, ${STATUS_WORD[state.status]}.`}
    >
      <SyntheticLabel />
      <Meter state={state} />
      {blocks}
      <SummaryBlock screen={screen} />
      <CounterpartyBlock screen={screen} />
      {state.lookups.map((lookup, i) => (
        <LookupCard key={`${lookup.what}-${lookup.key ?? i}`} lookup={lookup} currency={screen.currency} />
      ))}
      {state.lastRejection ? (
        <p
          role="status"
          className="rounded-control border border-warning bg-warning-subtle px-3 py-2 font-sans text-small text-warning"
        >
          <span className="font-mono text-label uppercase">Reply refused</span> — {state.lastRejection}. It still cost a turn.
        </p>
      ) : null}
      <DispositionCard state={state} />
      <RevealCard state={state} />
    </div>
  );
});

export default Board;
