/**
 * One account, whole.
 *
 * Every list in PIG used to be a terminus: the accounts table, the growth
 * cards and Piggy's evidence chips all named a record and then had nowhere to
 * send you. This is the page they point at, and it is assembled from the two
 * endpoints that already existed and had no caller — `/api/accounts/:id` for
 * the book and `/api/growth/accounts/:id` for the lifecycle projection.
 *
 * The ordering is deliberate. Two facts about a compute customer can make a
 * sale unlawful or a term renew itself against us — where the ultimate parent
 * sits, and whether a renewal-notice deadline has passed — so both are stated
 * above the fold as banners rather than left to be found in a table further
 * down.
 */
import { useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Link, useParams } from 'react-router-dom';
import {
  DEMAND_OPEN_STAGES,
  SUPPLY_OPEN_STAGES,
  type ActivityType,
  type ContractStatus,
  type ContractType,
  type CustomerLifecycleProjection,
  type CustomerRelationshipState,
  type PermissionGrant,
} from '@pig/core';
import {
  AlertTriangle,
  ArrowLeft,
  Bot,
  Building2,
  CalendarClock,
  CheckSquare,
  CircleAlert,
  CircleDollarSign,
  Clock3,
  ExternalLink,
  FileText,
  GitBranch,
  Globe2,
  Lock,
  Mail,
  MessageSquare,
  Pencil,
  Phone,
  RefreshCw,
  Server,
  ShieldAlert,
  StickyNote,
  UserPlus,
  Users,
} from 'lucide-react';
import { PiggyAskButton } from '@/components/PiggyChat';
import { PiggyMark } from '@/components/PiggyMark';
import {
  AccountSheet,
  ContactSheet,
  LogActivitySheet,
  canLogAgainstSide,
  type AccountRecord,
  type ContactRecord,
  type DemandDealRecord,
  type SupplyDealRecord,
} from '@/components/RecordSheets';
import {
  ContractStatusBadge,
  DealStageBadge,
  FacetBadge,
  RelationshipBadge,
  RenewalBadge,
  SideBadge,
} from '@/components/status';
import {
  Badge,
  Button,
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  ConfidenceBadge,
  EmptyState,
  Label,
  Section,
  Skeleton,
  Stat,
} from '@/components/ui';
import { ApiError, compactNumber, get, money, relativeTime, shortDate } from '@/lib/api';
import { can, canAny } from '@/lib/permissions';
import { usePiggyContext } from '@/lib/piggy-context';
import { usePageTitle } from '@/lib/title';
import { initials } from '@/lib/utils';

// ------------------------------------------------------------------- shapes

/** The account row in full, rather than the list projection RecordSheets edits. */
interface AccountDetailRecord extends AccountRecord {
  ultimateParentAccountId: string | null;
  ownershipVerifiedAt: string | null;
  ownerUserId: string | null;
}

/** `departedAt` is on the row but not on the shape the record sheet needs. */
interface ContactDetailRecord extends ContactRecord {
  departedAt: string | null;
}

interface ContractRecord {
  id: string;
  type: ContractType;
  status: ContractStatus;
  side: string;
  title: string;
  externalReference: string | null;
  effectiveAt: string | null;
  expiresAt: string | null;
  isAutoRenew: boolean;
  noticeDays: number | null;
  valueCents: number | null;
  currency: string;
  governingLaw: string | null;
}

interface ActivityRecord {
  id: string;
  type: ActivityType;
  subject: string | null;
  body: string | null;
  contactId: string | null;
  demandDealId: string | null;
  supplyDealId: string | null;
  actorAgent: string | null;
  /**
   * How a write Piggy made stays distinguishable from one a person typed.
   *
   * `executeMutation` stamps `actorAgent` only for an api-key principal, and
   * Piggy's approved writes run as the signed-in person — so an approved
   * proposal landed on this timeline reading as hand-typed, directly above a
   * seeded row that said "by piggy". The agent leaves two other marks and both
   * are read here: `pig_log_activity` writes a `piggy:` external id (unique,
   * so a retried tool call deduplicates), and every other write tool stamps
   * `meta.actorAgent` through `attributedToPiggy`.
   */
  externalId: string | null;
  meta: { actorAgent?: string | null } | null;
  occurredAt: string;
}

/**
 * A buying-group role.
 *
 * `deal_contacts` carries them — economic buyer, champion, procurement, legal,
 * blocker — and `/api/accounts/:id` now joins it through the account's demand
 * deals. Still optional on the wire, because the field is an enhancement: an
 * older API, or a supply-only account with no demand deals to join through,
 * leaves the panel falling back to the primary-contact link the deal row
 * already carries rather than rendering nothing.
 */
interface DealContactRecord {
  demandDealId: string;
  contactId: string;
  role: string | null;
}

interface AccountDetail {
  account: AccountDetailRecord;
  contacts: ContactDetailRecord[];
  demandDeals: DemandDealRecord[];
  supplyDeals: SupplyDealRecord[];
  contracts: ContractRecord[];
  activities: ActivityRecord[];
  dealContacts?: DealContactRecord[];
}

interface GrowthAccount {
  account: { id: string; name: string };
  lifecycle: CustomerLifecycleProjection;
  openDealCount: number;
}

interface TeamMember {
  id: string;
  name: string;
  title: string | null;
}

interface Me {
  permissions: PermissionGrant[];
}

// --------------------------------------------------------------------- page

export function Account() {
  const { id = '' } = useParams<{ id: string }>();
  const [accountSheet, setAccountSheet] = useState(false);
  const [contactSheet, setContactSheet] = useState<{ open: boolean; record?: ContactRecord }>({
    open: false,
  });
  const [activitySheet, setActivitySheet] = useState(false);

  const { data: me } = useQuery({ queryKey: ['me'], queryFn: () => get<Me>('/api/me') });
  const detail = useQuery({
    // Three segments, so the `['accounts']` invalidation every record sheet
    // already fires refreshes this page too, without colliding with the
    // two-segment list key.
    queryKey: ['accounts', id, 'detail'],
    queryFn: () => get<AccountDetail>(`/api/accounts/${id}`),
    enabled: Boolean(id),
    retry: false,
  });
  const growth = useQuery({
    queryKey: ['growth', 'account', id],
    queryFn: () => get<GrowthAccount>(`/api/growth/accounts/${id}`),
    enabled: Boolean(id),
    // A supply-only account has no lifecycle projection and answers 404. That
    // is an expected shape of this page, not a failure to retry.
    retry: false,
  });
  const team = useQuery({
    queryKey: ['team'],
    queryFn: () => get<TeamMember[]>('/api/team'),
    enabled: canAny(me, 'team:read'),
  });

  const account = detail.data?.account;
  usePageTitle(account?.name ?? 'Account');
  usePiggyContext(
    account ? { type: 'account', id: account.id, label: account.name } : undefined,
  );

  if (!canAny(me, 'book:read')) {
    return (
      <Restricted
        title="This account is restricted"
        description="Account records are behind book access. Ask a workspace admin to grant it in Settings."
      />
    );
  }

  if (detail.isLoading) return <LoadingAccount />;

  if (detail.error instanceof ApiError && detail.error.status === 404) {
    return (
      <Restricted
        icon={<Building2 />}
        title="No such account"
        description="This record has been removed, or the link was to an id that never existed."
      />
    );
  }

  if (detail.error || !detail.data || !account) {
    return (
      <div className="flex flex-col gap-3">
        <BackLink />
        <Card>
          <EmptyState
            title="Account unavailable"
            description={detail.error instanceof Error ? detail.error.message : 'The record could not be loaded.'}
            action={
              <Button variant="outline" onClick={() => void detail.refetch()}>
                <RefreshCw aria-hidden />
                Try again
              </Button>
            }
          />
        </Card>
      </div>
    );
  }

  const data = detail.data;
  const writable =
    account.side === 'both'
      ? can(me, 'deal:write', 'demand') || can(me, 'deal:write', 'supply')
      : can(me, 'deal:write', account.side);
  /*
   * A different question from `writable`, and asked of the same account. The
   * server checks `activity:write` against the side of the account the entry
   * names, so a demand rep on a supplier record must not be shown a control
   * that can only answer 403 — and `deal:write` is no proxy for it, since the
   * two capabilities are granted independently.
   */
  const canLogActivity = canLogAgainstSide(me, account.side);
  const owner = team.data?.find((member) => member.id === account.ownerUserId);
  const alarms = data.contracts.map((contract) => renewalAlarm(contract));
  const missed = alarms.filter((alarm) => alarm.state === 'missed');
  const lifecycle = growth.data?.lifecycle;

  return (
    <div className="flex flex-col gap-6">
      {/* The back link belongs to the header, not to the page rhythm: 24px of
          air between "All accounts" and the record it returns from reads as a
          block of its own rather than as the way out of this one. */}
      <div className="flex flex-col gap-3">
        <BackLink />
        <Header
          account={account}
          ownerLabel={ownerLabel(account.ownerUserId, owner, team.isLoading)}
          relationshipState={lifecycle?.relationshipState ?? null}
          writable={writable}
          onEdit={() => setAccountSheet(true)}
        />
      </div>

      {missed.length ? <MissedNoticeBanner alarms={missed} /> : null}
      <OwnershipPanel account={account} />

      <section className="grid grid-cols-2 gap-3 xl:grid-cols-4">
        <Stat
          label="Attention"
          value={lifecycle ? lifecycle.score : '—'}
          hint={lifecycle ? 'Deterministic, not a probability' : 'Demand accounts only'}
          tone={lifecycle && lifecycle.score >= 60 ? 'warning' : 'default'}
        />
        <Stat
          label="Open deals"
          value={openDealCount(data)}
          hint={`${data.demandDeals.length + data.supplyDeals.length} recorded in total`}
        />
        <Stat
          label="Contracted value"
          value={money(contractedValueCents(data.contracts))}
          hint={`${data.contracts.length} document${data.contracts.length === 1 ? '' : 's'} on file`}
        />
        <Stat
          label="Sold capacity"
          value={lifecycle ? `${compactNumber(lifecycle.soldCapacityGpuHours)} h` : '—'}
          hint={lifecycle ? `${compactNumber(lifecycle.heldCapacityGpuHours)} GPU-hours held` : 'No projection'}
        />
      </section>

      <div className="grid gap-3 xl:grid-cols-3 xl:items-start">
        {/*
          Sidebar first in the DOM, reordered right on wide screens: on a phone
          the useful reading order is "what state is this relationship in and
          who is in it" before three long tables of history.
        */}
        <aside className="flex min-w-0 flex-col gap-3 xl:order-2">
          <LifecyclePanel query={growth} />
          <ContactsPanel
            contacts={data.contacts}
            demandDeals={data.demandDeals}
            supplyDeals={data.supplyDeals}
            dealContacts={data.dealContacts}
            writable={writable}
            onAdd={() => setContactSheet({ open: true })}
            onEdit={(record) => setContactSheet({ open: true, record })}
          />
        </aside>

        <div className="flex min-w-0 flex-col gap-3 xl:order-1 xl:col-span-2">
          {/*
            A one-sided account gets one pipeline panel. Showing a supplier an
            empty "nothing has been sold to this account" card is a prompt to
            do something that would be wrong.
          */}
          {account.side !== 'supply' || data.demandDeals.length ? (
            <DemandDeals deals={data.demandDeals} contacts={data.contacts} />
          ) : null}
          {account.side !== 'demand' || data.supplyDeals.length ? (
            <SupplyDeals deals={data.supplyDeals} />
          ) : null}
          <Contracts contracts={data.contracts} alarms={alarms} />
          <Timeline
            activities={data.activities}
            contacts={data.contacts}
            demandDeals={data.demandDeals}
            supplyDeals={data.supplyDeals}
            canLog={canLogActivity}
            onLog={() => setActivitySheet(true)}
          />
        </div>
      </div>

      <AccountSheet
        open={accountSheet}
        onOpenChange={setAccountSheet}
        record={account}
        identity={me}
      />
      <ContactSheet
        open={contactSheet.open}
        onOpenChange={(open) => setContactSheet((state) => ({ ...state, open }))}
        record={contactSheet.record}
        defaultAccountId={account.id}
        identity={me}
      />
      {/*
        No refetch is wired to this sheet on purpose: it invalidates
        `['accounts']`, which prefix-matches this page's
        `['accounts', id, 'detail']` key, so the timeline above refreshes
        itself the moment the entry is written.
      */}
      <LogActivitySheet
        open={activitySheet}
        onOpenChange={setActivitySheet}
        identity={me}
        defaultAccountId={account.id}
      />
    </div>
  );
}

// ------------------------------------------------------------------- header

function BackLink() {
  return (
    <Link
      to="/accounts"
      className="tap -ml-1 inline-flex w-fit items-center gap-2 rounded-lg px-1 text-sm font-medium text-muted hover:text-fg"
    >
      <ArrowLeft className="size-4" aria-hidden />
      All accounts
    </Link>
  );
}

function Header({
  account,
  ownerLabel,
  relationshipState,
  writable,
  onEdit,
}: {
  account: AccountDetailRecord;
  ownerLabel: string;
  relationshipState: CustomerRelationshipState | null;
  writable: boolean;
  onEdit(): void;
}) {
  const type = account.supplierType ?? account.customerSegment;
  return (
    <Card>
      <CardHeader className="gap-3">
        <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
          <div className="flex min-w-0 items-start gap-3">
            <div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-accent-subtle text-base font-semibold text-accent-fg">
              {initials(account.name)}
            </div>
            <div className="min-w-0">
              <h1 className="break-words text-xl font-semibold tracking-tight sm:text-2xl">
                {account.name}
              </h1>
              <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-muted">
                {account.website ? (
                  <a
                    // `min-h-11` and a negative margin: a 20px-tall link in a
                    // record header is a target a thumb misses, and it was the
                    // only sub-44px control on this page. The margin keeps the
                    // metadata row visually where it was.
                    className="-my-3 inline-flex min-h-11 items-center gap-1 rounded-lg underline-offset-4 hover:text-fg hover:underline"
                    href={account.website}
                    target="_blank"
                    rel="noreferrer"
                  >
                    {account.domain ?? account.website}
                    <ExternalLink className="size-3.5" aria-hidden />
                  </a>
                ) : (
                  <span>{account.domain ?? 'No domain recorded'}</span>
                )}
              </div>
            </div>
          </div>
          <div className="flex shrink-0 flex-wrap gap-2">
            <PiggyAskButton
              context={{ type: 'account', id: account.id, label: account.name }}
              prompt="Summarise this account: where the commercial relationship stands, what needs a decision next, and which claims are inferred rather than recorded."
              label="Ask Piggy"
              variant="outline"
            />
            <Button variant="secondary" disabled={!writable} onClick={onEdit}>
              <Pencil aria-hidden />
              Edit
            </Button>
          </div>
        </div>

        <div className="flex flex-wrap gap-1.5">
          <SideBadge side={account.side} />
          {relationshipState ? <RelationshipBadge state={relationshipState} /> : null}
          {type ? <Badge tone="neutral">{humanise(type)}</Badge> : null}
          {/* The product's one provenance marker, rather than this page's copy
              of it: the accounts table and Piggy's evidence both draw an
              unverified record this way, and a record that looks differently
              sourced on two screens is a record nobody trusts on either. */}
          <ConfidenceBadge confidence={account.confidence} />
        </div>

        {account.description ? (
          <p className="max-w-3xl text-sm leading-6 text-muted">{account.description}</p>
        ) : null}

        <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
          <DetailTile label="Owner" value={ownerLabel} />
          <DetailTile label="Jurisdiction" value={account.jurisdiction} />
          <DetailTile label="Headquarters" value={account.country} />
          <DetailTile
            label="Last activity"
            value={account.lastActivityAt ? relativeTime(account.lastActivityAt) : null}
          />
        </div>
      </CardHeader>
    </Card>
  );
}

/**
 * One recorded field, as a tile.
 *
 * This was `Field`, one of eight label-and-value tiles the audit counted at
 * three numeral scales. It is now `Stat` at the inline size on an inset
 * surface, so a field on a record reads the same as a figure in a panel — and
 * an unrecorded one renders `Stat`'s muted em dash instead of a bold
 * "Not recorded", which set a missing fact in the same weight as a known one.
 */
function DetailTile({ label, value }: { label: string; value: string | null }) {
  return <Stat label={label} value={value} size="sm" surface="inset" />;
}

// ---------------------------------------------------------------- ownership

/**
 * Where the corporate tree ends, and whether that is somewhere else.
 *
 * US export controls on advanced computing apply a headquarters test that
 * reaches through ownership: an entity in an unrestricted country can be
 * restricted because of where its ultimate parent sits. A CRM that shows only
 * `country` cannot be used to answer the question, so the mismatch is stated
 * as a warning rather than left as two adjacent fields a reader must compare.
 */
function OwnershipPanel({ account }: { account: AccountDetailRecord }) {
  if (!account.ultimateParentName && !account.ultimateParentCountry) return null;
  const parentCountry = account.ultimateParentCountry;
  const mismatch = Boolean(parentCountry && account.country && parentCountry !== account.country);

  return (
    <Card className={mismatch ? 'border-warning/50' : undefined}>
      <CardHeader className="flex-row items-start gap-3 pb-3">
        {mismatch ? (
          <ShieldAlert className="mt-0.5 size-5 shrink-0 text-warning" aria-hidden />
        ) : (
          <Globe2 className="mt-0.5 size-5 shrink-0 text-muted" aria-hidden />
        )}
        <div className="min-w-0">
          <CardTitle className="text-base">
            {mismatch ? 'Ultimate parent sits in another country' : 'Corporate ownership'}
          </CardTitle>
          <p className="mt-1 text-sm leading-6 text-muted">
            {mismatch ? (
              <>
                {account.name} is registered in {account.country}, but its ultimate parent{' '}
                <strong className="font-medium text-fg">{account.ultimateParentName}</strong> is in{' '}
                <strong className="font-medium text-fg">{parentCountry}</strong>. Export controls
                apply a headquarters test that reaches through the corporate tree, so this
                account&rsquo;s own location does not settle whether a given allocation is lawful.
              </>
            ) : (
              <>
                Held by {account.ultimateParentName ?? 'an unnamed parent'}
                {parentCountry ? ` in ${parentCountry}` : ''}.
              </>
            )}
          </p>
        </div>
      </CardHeader>
      <CardContent className="pt-0">
        <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
          <DetailTile label="Ultimate parent" value={account.ultimateParentName} />
          <DetailTile label="Parent country" value={parentCountry} />
          {/*
            "Never verified" rather than an em dash: unverified ownership is a
            statement about this record, not an absent field, and it is the one
            an export-control question turns on.
          */}
          <DetailTile
            label="Ownership verified"
            value={
              account.ownershipVerifiedAt
                ? `${dateWithYear(account.ownershipVerifiedAt)} · ${relativeTime(account.ownershipVerifiedAt)}`
                : 'Never verified'
            }
          />
        </div>
      </CardContent>
    </Card>
  );
}

// ------------------------------------------------------------------ renewal

type RenewalState = 'not_applicable' | 'scheduled' | 'due_soon' | 'missed' | 'expired';

interface RenewalAlarm {
  contract: ContractRecord;
  noticeAt: Date | null;
  state: RenewalState;
  days: number | null;
}

/** Inside this window the deadline is imminent enough to be an alarm, not a date. */
const NOTICE_HORIZON_DAYS = 30;

/**
 * The renewal alarm, computed here rather than read from the response.
 *
 * `/api/contracts` returns `renewalState` alongside each row, but
 * `/api/accounts/:id` returns the raw contract records — so this mirrors
 * `renewalAlarm` in apps/api/src/services/contracts.ts, with one distinction
 * that page owes its reader: a deadline that has already passed is separated
 * from one that is merely close. They are not the same news.
 */
function renewalAlarm(contract: ContractRecord, now = new Date()): RenewalAlarm {
  if (!contract.isAutoRenew || !contract.expiresAt || contract.noticeDays == null) {
    return { contract, noticeAt: null, state: 'not_applicable', days: null };
  }
  const expiresAt = new Date(contract.expiresAt);
  const noticeAt = new Date(expiresAt.getTime() - contract.noticeDays * DAY_MS);
  const days = daysBetween(now, noticeAt);
  if (expiresAt <= now) return { contract, noticeAt, state: 'expired', days };
  if (noticeAt <= now) return { contract, noticeAt, state: 'missed', days };
  return {
    contract,
    noticeAt,
    state: days <= NOTICE_HORIZON_DAYS ? 'due_soon' : 'scheduled',
    days,
  };
}

function MissedNoticeBanner({ alarms }: { alarms: RenewalAlarm[] }) {
  return (
    <div
      role="alert"
      className="flex gap-3 rounded-2xl border border-danger/40 bg-danger/10 p-4 text-sm"
    >
      <AlertTriangle className="mt-0.5 size-5 shrink-0 text-danger" aria-hidden />
      <div className="min-w-0 space-y-2">
        <p className="font-semibold text-danger">
          {alarms.length === 1
            ? 'Renewal notice deadline has passed'
            : `${alarms.length} renewal notice deadlines have passed`}
        </p>
        {alarms.map((alarm) => (
          <p key={alarm.contract.id} className="leading-6 text-fg">
            <strong className="font-medium">{alarm.contract.title}</strong> — notice was due{' '}
            {dateWithYear(alarm.noticeAt)}, {agoOrIn(alarm.days)}. It expires{' '}
            {dateWithYear(alarm.contract.expiresAt)} and auto-renews unless notice was already
            served.
          </p>
        ))}
      </div>
    </div>
  );
}

/**
 * Renewal urgency in the shared vocabulary, with the two states it cannot say.
 *
 * `RenewalBadge` covers three of the five: no alarm, a notice already lapsed,
 * and a notice still scheduled. The middle two stay here because this page
 * separates a deadline that has *passed* from one that is merely close — they
 * are not the same news — and because a countdown is the whole point of the
 * cell. They carry the shared marks and tones so the distinction is the
 * wording, not a second colour language.
 *
 * The scheduled date is written by hand rather than delegated for the same
 * reason `dateWithYear` exists: `RenewalBadge` uses `shortDate`, and a notice
 * deadline two years out reading "Oct 13" is a question, not an answer.
 */
function RenewalCell({ alarm }: { alarm: RenewalAlarm }) {
  if (alarm.state === 'not_applicable') return <RenewalBadge state="not_applicable" />;
  if (alarm.state === 'expired') return <RenewalBadge state="expired" />;
  if (alarm.state === 'missed') {
    return (
      <Badge tone="danger">
        <AlertTriangle className="size-3.5" aria-hidden />
        Notice missed {agoOrIn(alarm.days)}
      </Badge>
    );
  }
  if (alarm.state === 'due_soon') {
    return (
      <Badge tone="warning">
        <CircleAlert className="size-3.5" aria-hidden />
        Notice {agoOrIn(alarm.days)}
      </Badge>
    );
  }
  return <span className="text-xs text-muted">Notice {dateWithYear(alarm.noticeAt)}</span>;
}

// -------------------------------------------------------------------- deals

function DemandDeals({
  deals,
  contacts,
}: {
  deals: DemandDealRecord[];
  contacts: ContactDetailRecord[];
}) {
  const ordered = useMemo(
    () => [...deals].sort((a, b) => dealRank(a.stage) - dealRank(b.stage) || b.updatedAt.localeCompare(a.updatedAt)),
    [deals],
  );

  return (
    <Panel
      icon={<CircleDollarSign className="size-4" aria-hidden />}
      title="Demand deals"
      count={deals.length}
      meta={deals.length ? `${money(sum(deals.map((deal) => deal.acvCents)))} recorded ACV` : undefined}
    >
      {ordered.length === 0 ? (
        <PanelEmpty
          title="Nothing sold to this account yet"
          description="A deal opened on the demand pipeline appears here."
        />
      ) : (
        <ul className="divide-y divide-border">
          {ordered.map((deal) => {
            const primary = contacts.find((contact) => contact.id === deal.primaryContactId);
            const closed = deal.stage === 'closed_won' || deal.stage === 'closed_lost';
            return (
              <li key={deal.id} className="flex flex-col gap-2 px-4 py-3.5 sm:px-5">
                <div className="flex flex-wrap items-start justify-between gap-x-3 gap-y-1">
                  <p className="min-w-0 font-medium">{deal.name}</p>
                  <span className="nums shrink-0 font-semibold">{money(deal.acvCents)}</span>
                </div>
                <div className="flex flex-wrap items-center gap-1.5">
                  <DealStageBadge side="demand" stage={deal.stage} />
                  <Badge tone="neutral">{humanise(deal.productLine)}</Badge>
                  {/* Executed paper is a process outcome, not good news: green
                      on every deal that has an MSA leaves nothing green for a
                      figure that is actually good. */}
                  {deal.msaExecuted ? <Badge tone="neutral">MSA</Badge> : null}
                  {deal.dpaExecuted ? <Badge tone="neutral">DPA</Badge> : null}
                </div>
                <p className="text-xs text-muted">
                  {closed ? 'Closed' : 'Expected close'} {dateWithYear(deal.expectedCloseDate)}
                  {deal.termMonths ? ` · ${deal.termMonths}-month term` : ''}
                  {primary ? ` · ${primary.fullName}` : ''}
                </p>
                {deal.closedReason ? (
                  <p className="text-xs leading-5 text-muted">{deal.closedReason}</p>
                ) : null}
              </li>
            );
          })}
        </ul>
      )}
    </Panel>
  );
}

function SupplyDeals({ deals }: { deals: SupplyDealRecord[] }) {
  return (
    <Panel
      icon={<Server className="size-4" aria-hidden />}
      title="Supply deals"
      count={deals.length}
    >
      {deals.length === 0 ? (
        <PanelEmpty
          title="No capacity sourced here"
          description="Supply-side deals appear here once one is opened against this account."
        />
      ) : (
        <ul className="divide-y divide-border">
          {deals.map((deal) => (
            <li key={deal.id} className="flex flex-col gap-2 px-4 py-3.5 sm:px-5">
              <div className="flex flex-wrap items-start justify-between gap-x-3 gap-y-1">
                <p className="min-w-0 font-medium">{deal.name}</p>
                <DealStageBadge side="supply" stage={deal.stage} />
              </div>
              <p className="text-xs text-muted">
                {deal.gpuCount && deal.gpuType ? `${deal.gpuCount}× ${deal.gpuType}` : 'Shape not recorded'}
                {deal.termMonths ? ` · ${deal.termMonths}-month term` : ''}
                {deal.availableFrom ? ` · available ${dateWithYear(deal.availableFrom)}` : ''}
              </p>
              {deal.rejectionReason ? (
                <p className="text-xs leading-5 text-warning">{deal.rejectionReason}</p>
              ) : null}
            </li>
          ))}
        </ul>
      )}
    </Panel>
  );
}

// ---------------------------------------------------------------- contracts

const CONTRACT_TYPE_LABELS: Record<ContractType, string> = {
  msa: 'MSA',
  dpa: 'DPA',
  sla: 'SLA',
  order_form: 'Order form',
  capacity_commitment: 'Capacity commitment',
  nda: 'NDA',
  amendment: 'Amendment',
};

function Contracts({
  contracts,
  alarms,
}: {
  contracts: ContractRecord[];
  alarms: RenewalAlarm[];
}) {
  // Loudest first: a passed deadline outranks an imminent one, which outranks
  // paper nobody has to think about this quarter.
  const ordered = useMemo(
    () =>
      [...alarms].sort(
        (a, b) =>
          alarmRank(a) - alarmRank(b) ||
          (a.noticeAt?.getTime() ?? Number.POSITIVE_INFINITY) -
            (b.noticeAt?.getTime() ?? Number.POSITIVE_INFINITY),
      ),
    [alarms],
  );

  return (
    <Panel
      icon={<FileText className="size-4" aria-hidden />}
      title="Contracts"
      count={contracts.length}
      action={
        contracts.length ? (
          // A Button rather than a bare link: this is a panel action, and a
          // 20px text link is a target a thumb misses.
          <Button size="sm" variant="ghost" asChild>
            <Link to="/contracts">All paper</Link>
          </Button>
        ) : undefined
      }
    >
      {ordered.length === 0 ? (
        <PanelEmpty
          title="No paper on file"
          description="Contracts created on the Contracts page appear here."
        />
      ) : (
        <ul className="divide-y divide-border">
          {ordered.map((alarm) => (
            <li key={alarm.contract.id} className="flex flex-col gap-2 px-4 py-3.5 sm:px-5">
              <div className="flex flex-wrap items-start justify-between gap-x-3 gap-y-1">
                {/*
                  A link, not a styled paragraph. Measured, this page's <main>
                  held six interactive elements and not one of them was a
                  contract — these rows carried a title, a type, a status, a
                  side and a notice date, read as clickable, and were plain
                  divs. "Open the contract this account is on" was a journey
                  with no route through the product at all. `?contract=` is the
                  URL contract the Contracts page now honours; it opens the same
                  detail sheet a row there opens.
                */}
                <Link
                  to={`/contracts?contract=${alarm.contract.id}`}
                  className="tap -my-2 flex min-w-0 items-center rounded-lg font-medium underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-surface"
                >
                  <span className="min-w-0 break-words">{alarm.contract.title}</span>
                </Link>
                <RenewalCell alarm={alarm} />
              </div>
              <div className="flex flex-wrap items-center gap-1.5">
                <Badge tone="neutral">{CONTRACT_TYPE_LABELS[alarm.contract.type]}</Badge>
                <ContractStatusBadge status={alarm.contract.status} />
                <SideBadge side={alarm.contract.side === 'supply' ? 'supply' : 'demand'} />
              </div>
              <p className="text-xs text-muted">
                {alarm.contract.effectiveAt ? `Effective ${dateWithYear(alarm.contract.effectiveAt)} · ` : ''}
                Expires {dateWithYear(alarm.contract.expiresAt)}
                {alarm.contract.noticeDays == null ? '' : ` · ${alarm.contract.noticeDays} days' notice`}
                {alarm.contract.externalReference ? ` · ${alarm.contract.externalReference}` : ''}
              </p>
            </li>
          ))}
        </ul>
      )}
    </Panel>
  );
}

// ---------------------------------------------------------------- lifecycle

function LifecyclePanel({
  query,
}: {
  query: { data?: GrowthAccount; isLoading: boolean; error: unknown };
}) {
  if (query.isLoading) {
    return (
      <Card className="p-4">
        <Skeleton className="h-40 rounded-md" />
      </Card>
    );
  }

  const notFound = query.error instanceof ApiError && query.error.status === 404;
  if (notFound || !query.data) {
    return (
      <Panel icon={<Clock3 className="size-4" aria-hidden />} title="Lifecycle">
        <PanelEmpty
          title={notFound ? 'Demand accounts only' : 'Lifecycle unavailable'}
          description={
            notFound
              ? 'Growth scores the demand book, so a supplier account carries no projection.'
              : 'The lifecycle projection could not be loaded for this account.'
          }
        />
      </Panel>
    );
  }

  const { lifecycle } = query.data;
  return (
    <Panel
      icon={<Clock3 className="size-4" aria-hidden />}
      title="Lifecycle"
      meta={`Rules ${lifecycle.rulesetVersion}`}
    >
      <div className="flex flex-col gap-3 px-4 pb-4 sm:px-5 sm:pb-5">
        {/* The panel figure, at the panel figure's size. This was a bespoke
            30px numeral — a third numeral scale on a page that already shows
            the same score at 30px in the KPI row above. */}
        <Stat
          surface="bare"
          size="md"
          label="Attention score"
          value={lifecycle.score}
          hint="Weighted evidence, not a win or churn probability."
        />
        <div className="flex flex-wrap gap-1.5">
          <RelationshipBadge state={lifecycle.relationshipState} />
          {lifecycle.facets.map((facet) => (
            <FacetBadge key={facet} facet={facet} />
          ))}
        </div>
        {lifecycle.signals.length ? (
          <ul className="space-y-2">
            {lifecycle.signals.map((signal) => (
              <li
                key={`${signal.code}:${signal.sourceRefs.map((ref) => ref.id).join(':')}`}
                className="flex gap-3 rounded-md bg-surface-2 p-3"
              >
                <span className="nums flex size-8 shrink-0 items-center justify-center rounded-md bg-surface text-xs font-semibold">
                  +{signal.weight}
                </span>
                <div className="min-w-0">
                  <p className="text-sm leading-5">{signal.explanation}</p>
                  <Label as="p" className="mt-1">
                    {signal.category} · {signal.sourceRefs.map((ref) => humanise(ref.type)).join(', ')}
                  </Label>
                </div>
              </li>
            ))}
          </ul>
        ) : (
          <p className="text-sm text-muted">
            No signal has met its evidence threshold for this account.
          </p>
        )}
        {lifecycle.blockers.map((blocker) => (
          <div key={blocker} className="flex gap-2 rounded-md bg-warning/10 p-3 text-sm text-warning">
            <AlertTriangle className="mt-0.5 size-4 shrink-0" aria-hidden />
            <span className="leading-5">{blocker}</span>
          </div>
        ))}
      </div>
    </Panel>
  );
}

// ----------------------------------------------------------------- contacts

function ContactsPanel({
  contacts,
  demandDeals,
  supplyDeals,
  dealContacts,
  writable,
  onAdd,
  onEdit,
}: {
  contacts: ContactDetailRecord[];
  demandDeals: DemandDealRecord[];
  supplyDeals: SupplyDealRecord[];
  dealContacts: DealContactRecord[] | undefined;
  writable: boolean;
  onAdd(): void;
  onEdit(record: ContactRecord): void;
}) {
  const roles = useMemo(
    () => buildDealRoles(demandDeals, supplyDeals, dealContacts),
    [demandDeals, supplyDeals, dealContacts],
  );
  const [expanded, setExpanded] = useState(false);
  // Ordered so the people who decide are not below the fold: a roster of two
  // dozen engineers otherwise buries the economic buyer.
  const ordered = useMemo(
    () =>
      [...contacts].sort(
        (a, b) =>
          Number(Boolean(b.isDecisionMaker)) - Number(Boolean(a.isDecisionMaker)) ||
          (roles.get(b.id)?.length ?? 0) - (roles.get(a.id)?.length ?? 0),
      ),
    [contacts, roles],
  );
  const visible = expanded ? ordered : ordered.slice(0, CONTACTS_INITIAL);

  return (
    <Panel
      icon={<Users className="size-4" aria-hidden />}
      title="Contacts"
      count={contacts.length}
      action={
        contacts.length ? (
          <Button size="sm" variant="ghost" disabled={!writable} onClick={onAdd}>
            <UserPlus aria-hidden />
            Add
          </Button>
        ) : undefined
      }
    >
      {contacts.length === 0 ? (
        <PanelEmpty
          title="Nobody recorded here"
          description="PIG never guesses an address from a name and a domain, so a contact is added once the relationship is known."
          action={
            <Button variant="outline" disabled={!writable} onClick={onAdd}>
              <UserPlus aria-hidden />
              Add a contact
            </Button>
          }
        />
      ) : (
        <ul className="divide-y divide-border">
          {visible.map((contact) => (
            <li key={contact.id} className="flex flex-col gap-1.5 px-4 py-3.5 sm:px-5">
              <div className="flex items-start justify-between gap-3">
                <div className="min-w-0">
                  <p className="truncate font-medium">{contact.fullName}</p>
                  <p className="truncate text-sm text-muted">
                    {contact.title ?? 'No title recorded'}
                  </p>
                </div>
                <Button
                  size="icon"
                  variant="ghost"
                  className="-my-1 shrink-0"
                  disabled={!writable}
                  title="Edit contact"
                  onClick={() => onEdit(contact)}
                >
                  <Pencil aria-hidden />
                  <span className="sr-only">Edit {contact.fullName}</span>
                </Button>
              </div>
              {/*
                A part somebody plays is identity, not status: it is neither
                good nor bad and nothing follows from it today, so it carries
                no colour and the word does the work. Departure is the one
                exception on this panel, because emailing a person who has left
                is a mistake somebody makes from this list.
              */}
              <div className="flex flex-wrap gap-1.5">
                {contact.isDecisionMaker ? <Badge tone="neutral">Decision maker</Badge> : null}
                {(roles.get(contact.id) ?? []).map((role) => (
                  <Badge key={role.label} tone="neutral" title={role.deals.join(' · ')}>
                    {role.label}
                    {role.deals.length > 1 ? <span className="nums opacity-70">×{role.deals.length}</span> : null}
                  </Badge>
                ))}
                {contact.departedAt ? (
                  <Badge tone="warning">
                    <CircleAlert className="size-3.5" aria-hidden />
                    Departed
                  </Badge>
                ) : null}
              </div>
              {contact.email ? (
                <a
                  className="inline-flex w-fit items-center gap-1.5 text-sm text-accent-fg underline-offset-4 hover:underline"
                  href={`mailto:${contact.email}`}
                >
                  <Mail className="size-3.5" aria-hidden />
                  {contact.email}
                </a>
              ) : (
                <p className="text-sm text-muted">No email recorded</p>
              )}
            </li>
          ))}
          {ordered.length > CONTACTS_INITIAL ? (
            <li className="p-3 sm:p-4">
              <Button variant="outline" className="w-full" onClick={() => setExpanded((value) => !value)}>
                {expanded ? 'Show fewer contacts' : `Show all ${ordered.length} contacts`}
              </Button>
            </li>
          ) : null}
        </ul>
      )}
    </Panel>
  );
}

/** A sidebar that runs three screens past the page it sits beside is broken. */
const CONTACTS_INITIAL = 8;

interface DealRole {
  label: string;
  /** Every deal this person plays that part on, for the badge's tooltip. */
  deals: string[];
}

/**
 * Who plays what part, from whichever source can answer.
 *
 * `deal_contacts` is authoritative and carries the buying-group role; the
 * deal's own `primaryContactId` is the fallback, because it is the one link
 * `/api/accounts/:id` reliably returns today.
 *
 * Keyed by role rather than by deal: someone who is the primary contact on
 * three deals is one fact about them, and rendering it as three identical
 * badges reads as a bug.
 */
function buildDealRoles(
  demandDeals: DemandDealRecord[],
  supplyDeals: SupplyDealRecord[],
  dealContacts: DealContactRecord[] | undefined,
): Map<string, DealRole[]> {
  const deals = [...demandDeals, ...supplyDeals];
  const dealNames = new Map(deals.map((deal) => [deal.id, deal.name]));

  const roles = new Map<string, DealRole[]>();
  const add = (contactId: string, label: string, dealName: string) => {
    const existing = roles.get(contactId) ?? [];
    if (!roles.has(contactId)) roles.set(contactId, existing);
    const role = existing.find((candidate) => candidate.label === label);
    if (role) {
      if (!role.deals.includes(dealName)) role.deals.push(dealName);
      return;
    }
    existing.push({ label, deals: [dealName] });
  };

  for (const link of dealContacts ?? []) {
    if (!link.role) continue;
    add(link.contactId, humanise(link.role), dealNames.get(link.demandDealId) ?? 'Demand deal');
  }

  for (const deal of deals) {
    if (!deal.primaryContactId) continue;
    add(deal.primaryContactId, 'Primary contact', deal.name);
  }

  return roles;
}

// ----------------------------------------------------------------- timeline

/** Enough to see the shape of the relationship without an endless scroll. */
const TIMELINE_INITIAL = 12;

const ACTIVITY_ICONS: Record<ActivityType, typeof Mail> = {
  note: StickyNote,
  email: Mail,
  call: Phone,
  meeting: Users,
  slack: MessageSquare,
  buzz: MessageSquare,
  stage_change: GitBranch,
  contract_event: FileText,
  task: CheckSquare,
  agent_action: Bot,
};

function Timeline({
  activities,
  contacts,
  demandDeals,
  supplyDeals,
  canLog,
  onLog,
}: {
  activities: ActivityRecord[];
  contacts: ContactDetailRecord[];
  demandDeals: DemandDealRecord[];
  supplyDeals: SupplyDealRecord[];
  canLog: boolean;
  onLog(): void;
}) {
  const [expanded, setExpanded] = useState(false);
  const visible = expanded ? activities : activities.slice(0, TIMELINE_INITIAL);
  const months = useMemo(() => groupByMonth(visible), [visible]);

  const contactName = (id: string | null) =>
    id ? contacts.find((contact) => contact.id === id)?.fullName ?? null : null;
  const dealName = (activity: ActivityRecord) => {
    const id = activity.demandDealId ?? activity.supplyDealId;
    if (!id) return null;
    return [...demandDeals, ...supplyDeals].find((deal) => deal.id === id)?.name ?? null;
  };

  return (
    <Panel
      icon={<CalendarClock className="size-4" aria-hidden />}
      title="Activity"
      count={activities.length}
      meta={activities.length ? 'Most recent first' : undefined}
      action={
        activities.length ? (
          <Button size="sm" variant="ghost" disabled={!canLog} onClick={onLog} title={logTitle(canLog)}>
            <StickyNote aria-hidden />
            Log
          </Button>
        ) : undefined
      }
    >
      {activities.length === 0 ? (
        <PanelEmpty
          title="Nothing logged yet"
          description="Anything recorded from the pipeline, Slack or Piggy lands here — and a call you have just had can be written down now."
          action={
            <Button variant="outline" disabled={!canLog} onClick={onLog} title={logTitle(canLog)}>
              <StickyNote aria-hidden />
              Log activity
            </Button>
          }
        />
      ) : (
        <div className="flex flex-col gap-6 px-4 pb-5 sm:px-5">
          {months.map(([month, entries]) => (
            <section key={month} aria-label={month}>
              <Label
                as="h3"
                className="sticky top-[var(--app-header-h)] z-10 -mx-4 bg-surface/95 px-4 py-2 backdrop-blur sm:-mx-5 sm:px-5"
              >
                {month}
              </Label>
              <ol className="mt-2 space-y-4">
                {entries.map((activity, index) => {
                  const Icon = ACTIVITY_ICONS[activity.type];
                  // The rail joins one entry to the next; drawing it below the
                  // final entry leaves a stub pointing at nothing.
                  const joined = index < entries.length - 1;
                  const person = contactName(activity.contactId);
                  const deal = dealName(activity);
                  return (
                    <li key={activity.id} className="flex gap-3">
                      <div className="flex flex-col items-center">
                        <span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-surface-2 text-muted">
                          <Icon className="size-4" aria-hidden />
                        </span>
                        {/* The rail is what makes a list of entries read as one history. */}
                        {joined ? <span className="mt-1 w-px flex-1 bg-border" aria-hidden /> : null}
                      </div>
                      <div className="min-w-0 flex-1 pb-1">
                        <p className="font-medium leading-6">{activity.subject ?? humanise(activity.type)}</p>
                        {activity.body ? (
                          <p className="mt-1 max-w-prose text-sm leading-6 text-muted">{activity.body}</p>
                        ) : null}
                        <p className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted">
                          <span title={new Date(activity.occurredAt).toLocaleString()}>
                            {shortDate(activity.occurredAt)} · {relativeTime(activity.occurredAt)}
                          </span>
                          {person ? <span>· {person}</span> : null}
                          {deal ? <span className="truncate">· {deal}</span> : null}
                          <ActorNote activity={activity} />
                        </p>
                      </div>
                    </li>
                  );
                })}
              </ol>
            </section>
          ))}
          {activities.length > TIMELINE_INITIAL ? (
            <Button variant="outline" className="w-full" onClick={() => setExpanded((value) => !value)}>
              {expanded
                ? 'Show recent activity only'
                : `Show all ${activities.length} entries`}
            </Button>
          ) : null}
        </div>
      )}
    </Panel>
  );
}

/**
 * Why the control is dead, said on the control rather than after the 403.
 *
 * `activity:write` is held per team and checked against this account's side, so
 * "you cannot write here" is the honest sentence — not "you cannot write".
 */
function logTitle(canLog: boolean): string | undefined {
  return canLog ? undefined : 'Logging activity against this account needs write access to its side of the book';
}

/**
 * Whether Piggy wrote this entry.
 *
 * Two marks, because the agent leaves two: `pig_log_activity` writes a
 * `piggy:` external id, and the four write tools that change a record stamp
 * `meta.actorAgent` as the mutation's own activity row is inserted. Neither
 * sets the `actor_agent` column, which is reserved for an api-key principal —
 * so a timeline that read only that column showed an approved proposal as if a
 * person had typed it.
 */
function writtenByPiggy(activity: ActivityRecord): boolean {
  return (
    activity.actorAgent === 'piggy' ||
    activity.meta?.actorAgent === 'piggy' ||
    Boolean(activity.externalId?.startsWith('piggy:'))
  );
}

/**
 * Who wrote the entry, where it is worth saying.
 *
 * Only a machine gets named. An entry with no attribution was typed by a
 * person, and "by Priya" on ninety per cent of a timeline is ninety per cent
 * noise — the exception is the interesting fact, and on this product the
 * interesting fact is that the agent did it and a human approved it.
 */
function ActorNote({ activity }: { activity: ActivityRecord }) {
  if (writtenByPiggy(activity)) {
    return (
      <span className="inline-flex items-center gap-1">
        ·<PiggyMark className="size-3.5 shrink-0" /> by Piggy
      </span>
    );
  }
  return activity.actorAgent ? <span>· by {activity.actorAgent}</span> : null;
}

function groupByMonth(activities: ActivityRecord[]): [string, ActivityRecord[]][] {
  const months = new Map<string, ActivityRecord[]>();
  for (const activity of activities) {
    const label = MONTH_FORMAT.format(new Date(activity.occurredAt));
    const existing = months.get(label);
    if (existing) existing.push(activity);
    else months.set(label, [activity]);
  }
  return [...months];
}

// -------------------------------------------------------------- furniture

/**
 * A card that a `Section` titles.
 *
 * The heading, the count and the fold are the primitive's now; the card, the
 * icon and the meta line are this page's. `level={2}` puts the panels one step
 * under the record's `<h1>` and leaves `h3` free for the timeline's months,
 * which were competing with the panel titles at the same level.
 */
function Panel({
  icon,
  title,
  count,
  meta,
  action,
  children,
}: {
  icon: React.ReactNode;
  title: string;
  count?: number;
  meta?: string;
  action?: React.ReactNode;
  children: React.ReactNode;
}) {
  return (
    <Card>
      <div className="p-4 pb-3 sm:p-5 sm:pb-3">
        <Section
          level={2}
          title={
            <span className="flex min-w-0 items-center gap-2">
              <span className="shrink-0 text-muted">{icon}</span>
              {title}
            </span>
          }
          count={count}
          /*
            Both, where both are given. The header used to let an action replace
            the meta line, which was harmless while no panel supplied the two —
            and would have silently dropped "Most recent first" from the timeline
            the moment it grew a Log button. Where they compete for the same row
            on a phone the meta gives way, because the action is the half that
            cannot be recovered by reading further down the panel.
          */
          action={
            action || meta ? (
              // `-my-2` so a 44px control does not inflate a 24px heading row
              // into a 44px one and open a hole above the first list entry.
              <div className="-my-2 flex shrink-0 items-center gap-2">
                {meta ? (
                  <span className={`text-xs text-muted${action ? ' hidden sm:inline' : ''}`}>
                    {meta}
                  </span>
                ) : null}
                {action}
              </div>
            ) : undefined
          }
        />
      </div>
      {children}
    </Card>
  );
}

/**
 * A panel with nothing in it, said the way the rest of the product says it.
 *
 * Seven of these existed; this page held the least finished of them — a
 * centred grey paragraph with no title, no shape and a different height in
 * every panel. `panel` is the size `EmptyState` names for a card body, and it
 * is used for every one of them including the two in the sidebar: the columns
 * collapse into a single stack below `xl`, so a size chosen by which column a
 * panel sits in is a page that looks designed by two people on a phone.
 */
function PanelEmpty({
  title,
  description,
  action,
}: {
  title: string;
  description: string;
  action?: React.ReactNode;
}) {
  return <EmptyState size="panel" title={title} description={description} action={action} />;
}

function Restricted({
  icon,
  title,
  description,
}: {
  icon?: React.ReactNode;
  title: string;
  description: string;
}) {
  // No usePageTitle here: the page already sets one, and a second call from a
  // child wins the mount order and loses the unmount race, leaving the tab
  // named after whichever effect happened to run last.
  return (
    <div className="flex flex-col gap-3">
      <BackLink />
      <Card>
        <CardContent className="pt-5">
          <EmptyState
            icon={icon ?? <Lock />}
            title={title}
            description={description}
            action={
              <Button variant="outline" asChild>
                <Link to="/accounts">Back to accounts</Link>
              </Button>
            }
          />
        </CardContent>
      </Card>
    </div>
  );
}

function LoadingAccount() {
  return (
    <div className="flex flex-col gap-6" role="status" aria-live="polite">
      <span className="sr-only">Loading account</span>
      <Skeleton className="h-4 w-28" />
      <Skeleton className="h-48 rounded-2xl" />
      <div className="grid grid-cols-2 gap-3 xl:grid-cols-4">
        {[0, 1, 2, 3].map((key) => (
          <Skeleton key={key} className="h-24 rounded-2xl" />
        ))}
      </div>
      <div className="grid gap-3 xl:grid-cols-3">
        <Skeleton className="h-72 rounded-2xl xl:order-2" />
        <Skeleton className="h-72 rounded-2xl xl:order-1 xl:col-span-2" />
      </div>
    </div>
  );
}

// ------------------------------------------------------------------ helpers

const DAY_MS = 86_400_000;

const MONTH_FORMAT = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' });

/**
 * A date carrying its year.
 *
 * `shortDate` deliberately omits the year, which is right in a list of things
 * happening this quarter and wrong here: this page shows contract expiries and
 * notice deadlines two years out, where "Oct 13" is a question rather than an
 * answer.
 */
const DATE_FORMAT = new Intl.DateTimeFormat('en-US', {
  month: 'short',
  day: 'numeric',
  year: 'numeric',
});

function dateWithYear(value: string | Date | null | undefined): string {
  if (!value) return '—';
  const date = typeof value === 'string' ? new Date(value) : value;
  if (Number.isNaN(date.getTime())) return '—';
  return DATE_FORMAT.format(date);
}

/** Whole days from `from` to `to`, negative when `to` is in the past. */
function daysBetween(from: Date, to: Date): number {
  return Math.round((to.getTime() - from.getTime()) / DAY_MS);
}

function agoOrIn(days: number | null): string {
  if (days == null) return '';
  if (days === 0) return 'today';
  const magnitude = Math.abs(days);
  const unit = magnitude === 1 ? 'day' : 'days';
  return days < 0 ? `${magnitude} ${unit} ago` : `in ${magnitude} ${unit}`;
}

/**
 * Who owns this account, said honestly.
 *
 * Resolving the name needs `/api/team`, which is behind `team:read` — so an
 * unresolved id means one of three different things, and printing an em dash
 * for all three would tell a viewer the account is unowned when it is only
 * unreadable to them.
 *
 * `loading` must come from `isLoading` rather than `isPending`: a query
 * disabled for want of `team:read` stays pending forever, and a "Loading…"
 * that never resolves is the worst of the three answers.
 */
function ownerLabel(
  ownerUserId: string | null,
  owner: TeamMember | undefined,
  loading: boolean,
): string {
  if (!ownerUserId) return 'Unassigned';
  if (owner) return owner.name;
  return loading ? 'Loading…' : 'Not shown';
}

function humanise(value: string): string {
  // `ai` is cased because the segment enum is `applied_ai_startup`, and
  // "Applied ai startup" on a record about GPU capacity reads as a typo.
  const spaced = value.replaceAll('_', ' ').replace(/\bai\b/g, 'AI');
  return spaced.charAt(0).toUpperCase() + spaced.slice(1);
}

function sum(values: (number | null)[]): number {
  return values.reduce((total: number, value) => total + (value ?? 0), 0);
}

function contractedValueCents(contracts: ContractRecord[]): number | null {
  const executed = contracts.filter((contract) => contract.status === 'executed');
  if (!executed.some((contract) => contract.valueCents != null)) return null;
  return sum(executed.map((contract) => contract.valueCents));
}

/**
 * "Open" means what the ontology says it means.
 *
 * Re-deriving it here as "not closed" would quietly disagree with the pipeline
 * pages on the stages that are neither open nor closed — a supply deal in
 * `renewal`, a demand deal in `expansion` — and two different counts of the
 * same account is how a number stops being trusted.
 */
function openDealCount(detail: AccountDetail): number {
  const openDemand = detail.demandDeals.filter((deal) => DEMAND_OPEN_STAGES.includes(deal.stage)).length;
  const openSupply = detail.supplyDeals.filter((deal) => SUPPLY_OPEN_STAGES.includes(deal.stage)).length;
  return openDemand + openSupply;
}

function dealRank(stage: DemandDealRecord['stage']): number {
  return stage === 'closed_won' || stage === 'closed_lost' ? 1 : 0;
}

function alarmRank(alarm: RenewalAlarm): number {
  return alarm.state === 'missed' ? 0 : alarm.state === 'due_soon' ? 1 : alarm.state === 'scheduled' ? 2 : 3;
}

