/**
 * Capacity — availability, the matcher, and the provider market.
 *
 * The matcher is the screen that justifies the product: given what a customer
 * wants, what have we already bought that could serve them, and would selling
 * it make money? No generic CRM can answer either half.
 *
 * The third tab answers the other half of a broker's day — what the market is
 * asking for the same GPU — because a cost without a market price is not a
 * position, it is a number.
 */
import { useMemo, useState } from 'react';
import { useMutation, useQuery } from '@tanstack/react-query';
import {
  AlertTriangle,
  Boxes,
  Lock,
  Plus,
  Search,
  Server,
  ShieldCheck,
  Zap,
} from 'lucide-react';
import {
  compactNumber,
  dateRange,
  get,
  percent,
  post,
  relativeTime,
  shortDate,
  unitPrice,
} from '@/lib/api';
import { usePageTitle } from '@/lib/title';
import { useIdentity } from '@/lib/identity';
import { can, canAny } from '@/lib/permissions';
import { usePiggyContext } from '@/lib/piggy-context';
import {
  AllocationSheet,
  type AvailabilityRow,
  type MatchRow,
} from '@/components/AllocationSheet';
import { CommitmentSheet } from '@/components/RecordSheets';
import {
  Badge,
  Button,
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  EmptyState,
  Input,
  Label,
  Skeleton,
  Stat,
} from '@/components/ui';
import { FormField } from '@/components/ui/form-field';
import { PageHeader } from '@/components/ui/page-header';
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { UtilisationBar } from '@/components/ui/utilisation-bar';

type CapacityTab = 'available' | 'match' | 'inventory';

const TABS: { value: CapacityTab; label: string; shortLabel: string }[] = [
  { value: 'available', label: 'Availability', shortLabel: 'Availability' },
  { value: 'match', label: 'Match a requirement', shortLabel: 'Match' },
  { value: 'inventory', label: 'Provider inventory', shortLabel: 'Inventory' },
];

/**
 * What Piggy is told it is looking at, per tab.
 *
 * The dock otherwise reports the bare route, so it would answer about the
 * matcher while the user is staring at provider prices. The relay caps the
 * label at 240 characters and the prompt reads it as a screen title, so these
 * are titles rather than sentences.
 */
const PIGGY_LABELS: Record<CapacityTab, string> = {
  available: 'Capacity — availability of committed blocks',
  match: 'Capacity — matching a customer requirement against the book',
  inventory: 'Capacity — provider inventory, and its spread over our cost',
};

/**
 * Radix forbids an empty option value, and "no filter" still needs a value the
 * user can choose their way back to.
 */
const ANY_GPU_TYPE = 'any';

/** Fabrics the API counts as high-speed; mirrored from capacityMeetsRequirement. */
const FAST_FABRICS = ['Infiniband', 'RoCE', 'NVLink'];

export function Capacity() {
  usePageTitle('Capacity');
  const [tab, setTab] = useState<CapacityTab>('available');
  usePiggyContext({ type: 'page', route: '/capacity', label: PIGGY_LABELS[tab] });
  const [allocation, setAllocation] = useState<{
    open: boolean;
    preferredCommitmentId?: string;
    matches?: MatchRow[];
    defaultGpuHours?: number;
  }>({ open: false });
  const [recordingCommitment, setRecordingCommitment] = useState(false);
  const me = useIdentity();
  /*
   * Three different questions, and conflating them is what F3 exists to prevent.
   *
   * Every figure on this page — cost per GPU-hour, break-even, the matcher's
   * verdict — is now gated on `economics:read`, so without it the page has
   * nothing to render and every request answers 403. Say so, rather than
   * showing empty cards and a network error.
   *
   * Writing is two capabilities, on opposite sides of the book. The allocate
   * and hold buttons post to `/api/allocations`, which the server authorises on
   * `deal:write` for the DEMAND team — selling hours is a seller's act. The
   * "Record commitment" button posts to `/api/commitments`, which is
   * `commitment:write` and supply-team lead-and-above: buying the block is the
   * other desk's authority entirely. A reader may well hold one and not the
   * other, so the two are asked separately and gate their own buttons.
   *
   * `canAny` rather than `can` for the read: read grants are platform-wide by
   * construction (see READ_CAPABILITIES), so there is no team to name.
   */
  const readable = canAny(me, 'economics:read');
  const writable = can(me, 'deal:write', 'demand');
  const canCommit = can(me, 'commitment:write', 'supply');

  if (!readable) {
    return (
      <div className="space-y-6">
        <PageHeader title="Capacity" />
        <Card>
          <CardContent className="pt-5">
            <EmptyState
              icon={<Lock className="h-8 w-8" />}
              title="Capacity economics are restricted"
              description="Supplier cost and break-even pricing need the economics permission. Ask a platform administrator for supply or demand team membership."
            />
          </CardContent>
        </Card>
      </div>
    );
  }

  return (
    <div className="space-y-6 pb-[calc(5.5rem+var(--safe-bottom))] md:pb-0">
      <PageHeader
        title="Capacity"
        description="What we hold, what is sold, what is still sellable — and what providers are charging for the same GPUs."
        actions={
          <RecordCommitmentButton
            canCommit={canCommit}
            onRecord={() => setRecordingCommitment(true)}
          />
        }
      />

      {/* A segmented control rather than tabs — it reads correctly at phone
          width, where a tab row would either wrap or scroll. */}
      <div
        role="tablist"
        aria-label="Capacity views"
        className="inline-flex w-full rounded-lg bg-surface-2 p-1 sm:w-auto"
      >
        {TABS.map(({ value, label, shortLabel }) => (
          <button
            key={value}
            role="tab"
            aria-selected={tab === value}
            aria-label={label}
            onClick={() => setTab(value)}
            className={[
              'tap flex-1 rounded-md px-3 text-sm font-medium transition-colors sm:flex-none sm:px-4',
              tab === value ? 'bg-surface text-fg shadow-sm' : 'text-muted',
            ].join(' ')}
          >
            {/* Three tabs will not fit at 393px with "Match a requirement"
                spelled out, and a wrapping segmented control looks broken. */}
            <span className="sm:hidden">{shortLabel}</span>
            <span className="hidden sm:inline">{label}</span>
          </button>
        ))}
      </div>

      {tab === 'available' ? (
        <Availability
          writable={writable}
          canCommit={canCommit}
          onRecordCommitment={() => setRecordingCommitment(true)}
          onAllocate={(preferredCommitmentId) =>
            setAllocation({ open: true, preferredCommitmentId })
          }
        />
      ) : tab === 'match' ? (
        <Matcher
          writable={writable}
          onAllocate={(preferredCommitmentId, matches, defaultGpuHours) =>
            setAllocation({ open: true, preferredCommitmentId, matches, defaultGpuHours })
          }
        />
      ) : (
        <ProviderInventory />
      )}
      <AllocationSheet
        {...allocation}
        onOpenChange={(open) => setAllocation((state) => ({ ...state, open }))}
      />
      {/*
        Mounted beside the allocation sheet rather than inside the empty state
        that opens it: recording the first block makes that empty state
        disappear, and a sheet that unmounts underneath its own success toast
        closes with a jump.
      */}
      <CommitmentSheet
        open={recordingCommitment}
        onOpenChange={setRecordingCommitment}
        identity={me}
      />
    </div>
  );
}

/**
 * The one affordance for `commitment:write` in the product.
 *
 * Rendered disabled rather than hidden when the capability is missing: the
 * availability empty state describes recording a commitment as the next step,
 * and a next step that silently is not there reads as a broken page rather than
 * as somebody else's job. The title says whose job it is.
 */
function RecordCommitmentButton({
  canCommit,
  onRecord,
  className,
  variant = 'primary',
}: {
  canCommit: boolean;
  onRecord(): void;
  className?: string;
  variant?: 'primary' | 'outline';
}) {
  return (
    <Button
      variant={variant}
      className={className}
      disabled={!canCommit}
      onClick={onRecord}
      title={canCommit ? undefined : 'Recording committed capacity needs supply-team lead access'}
    >
      <Plus data-icon="inline-start" aria-hidden />
      Record commitment
    </Button>
  );
}

function Availability({
  writable,
  canCommit,
  onRecordCommitment,
  onAllocate,
}: {
  writable: boolean;
  canCommit: boolean;
  onRecordCommitment(): void;
  onAllocate(id: string): void;
}) {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ['availability'],
    queryFn: () => get<AvailabilityRow[]>('/api/capacity/availability'),
  });

  if (isLoading) return <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">{Array.from({ length: 5 }).map((_, index) => <Skeleton key={index} className="h-60" />)}</div>;

  if (error) {
    return <Card><CardContent className="flex flex-col items-center gap-4 pt-6"><EmptyState icon={<AlertTriangle className="size-8" />} title="Could not load capacity" description={error instanceof Error ? error.message : 'Availability is unavailable.'} /><Button variant="outline" onClick={() => void refetch()}>Try again</Button></CardContent></Card>;
  }

  if (!data || data.length === 0) {
    return (
      <Card>
        <CardContent className="pt-5">
          <EmptyState
            icon={<Server className="h-8 w-8" />}
            title="No live capacity commitments"
            description={
              canCommit
                ? 'Record what capacity you have committed to buy, and this view shows how much of each block is sold, held, and still available.'
                : 'Once a supply lead records what capacity has been committed to, this view shows how much of each block is sold, held, and still available.'
            }
            action={
              <RecordCommitmentButton canCommit={canCommit} onRecord={onRecordCommitment} />
            }
          />
        </CardContent>
      </Card>
    );
  }

  return (
    <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
      {data.map((row) => (
        <CapacityCard key={row.commitmentId} row={row} writable={writable} onAllocate={() => onAllocate(row.commitmentId)} />
      ))}
    </div>
  );
}

function CapacityCard({ row, writable, onAllocate }: { row: AvailabilityRow; writable: boolean; onAllocate(): void }) {
  const soldPct = row.totalGpuHours > 0 ? row.soldGpuHours / row.totalGpuHours : 0;
  const heldPct = row.totalGpuHours > 0 ? row.heldGpuHours / row.totalGpuHours : 0;

  return (
    /*
     * `min-w-0` is load-bearing. A grid item defaults to `min-width: auto`,
     * which means it refuses to shrink below its content — and `truncate` sets
     * `white-space: nowrap`, so a long title becomes unshrinkable content and
     * widens the whole track. The result is a page that scrolls sideways on a
     * phone. This is the fix, and it is needed on every grid child that
     * truncates.
     */
    <Card className="min-w-0">
      <CardHeader>
        <div className="flex items-start justify-between gap-2">
          <CardTitle className="min-w-0 break-words text-base leading-snug">{row.name}</CardTitle>
          <Badge tone={row.securityTier === 'secure_cloud' ? 'accent' : 'neutral'}>
            {row.securityTier === 'secure_cloud' ? 'Secure' : 'Community'}
          </Badge>
        </div>
        {/*
          `dateRange`, not two `shortDate`s: a nine-month block running June to
          the following March printed "Jun 14–Mar 11, 2027", a window that
          reads as running backwards because only the end carried its year.
        */}
        <p className="text-xs text-muted">
          {row.gpuCount}× {row.gpuType} · {row.interconnectType} ·{' '}
          {dateRange(row.startsAt, row.endsAt)}
        </p>
      </CardHeader>
      <CardContent className="space-y-3">
        <div className="space-y-2">
          <UtilisationBar
            sold={row.soldGpuHours}
            held={row.heldGpuHours}
            total={row.totalGpuHours}
            label={row.name}
          />
          <div className="flex justify-between text-xs text-muted">
            <span>{percent(soldPct)} sold</span>
            {row.heldGpuHours > 0 ? <span>{percent(heldPct)} held</span> : null}
            <span className="nums">{compactNumber(row.availableGpuHours)} hrs sellable</span>
          </div>
        </div>

        <dl className="grid grid-cols-2 items-baseline gap-x-3 gap-y-2">
          <Label as="dt">Cost</Label>
          <dd className="nums text-right text-sm font-medium">{unitPrice(row.costPerGpuHourCents)}/GPU-hr</dd>
          <Label as="dt">Break even</Label>
          <dd className="nums text-right text-sm font-medium">
            {row.breakEvenPriceCents == null
              ? 'Fully sold'
              : row.breakEvenPriceCents === 0
                ? 'Cost covered'
                : `${unitPrice(row.breakEvenPriceCents)}/GPU-hr`}
          </dd>
        </dl>
        <Button variant="outline" className="w-full" disabled={!writable || row.availableGpuHours <= 0} onClick={onAllocate} title={!writable ? 'Allocating capacity needs demand-team write access' : undefined}>
          <ShieldCheck data-icon="inline-start" aria-hidden />
          Allocate or hold
        </Button>
      </CardContent>
    </Card>
  );
}

function Matcher({ writable, onAllocate }: { writable: boolean; onAllocate(id: string, matches: MatchRow[], defaultGpuHours?: number): void }) {
  const [form, setForm] = useState({
    gpuType: ANY_GPU_TYPE,
    gpuCount: '64',
    totalGpuHours: '',
    startsAt: '',
    endsAt: '',
    requiresHighSpeedInterconnect: true,
    maxPrice: '',
  });

  /*
   * The same query the availability tab runs, so it is already in cache — and
   * it is the only honest source for the GPU-type control. The matcher compares
   * `gpuType` for exact equality against the commitment rows this endpoint
   * returns, so any value not in this list can only ever return nothing.
   */
  const { data: blocks, isLoading: blocksLoading } = useQuery({
    queryKey: ['availability'],
    queryFn: () => get<AvailabilityRow[]>('/api/capacity/availability'),
  });

  const gpuTypeOptions = useMemo(() => summariseGpuTypes(blocks ?? []), [blocks]);

  const mutation = useMutation({
    mutationFn: () =>
      post<MatchRow[]>('/api/capacity/match', {
        gpuType: form.gpuType === ANY_GPU_TYPE ? undefined : form.gpuType,
        gpuCount: Number(form.gpuCount) || 1,
        totalGpuHours: form.totalGpuHours ? Number(form.totalGpuHours) : undefined,
        startsAt: form.startsAt ? new Date(`${form.startsAt}T00:00:00`).toISOString() : undefined,
        endsAt: form.endsAt ? new Date(`${form.endsAt}T23:59:59`).toISOString() : undefined,
        requiresHighSpeedInterconnect: form.requiresHighSpeedInterconnect,
        maxPricePerGpuHourCents: form.maxPrice
          ? Math.round(Number(form.maxPrice) * 100)
          : undefined,
      }),
  });

  return (
    <div className="space-y-6">
      <Card>
        <CardHeader>
          <CardTitle className="text-base">What does the customer need?</CardTitle>
          <p className="text-sm text-muted">Match against capacity already under commitment. The allocation ledger remains the final authority when you save.</p>
        </CardHeader>
        <CardContent>
          <form
            className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4"
            aria-label="Capacity requirement"
            onSubmit={(event) => {
              event.preventDefault();
              mutation.mutate();
            }}
          >
            <FormField label="GPU type">
              {/*
                * A Select, not free text. Hardware identifiers are matched
                * verbatim upstream, so "H100" — which is what anyone types —
                * silently matched nothing and sent the user to an empty state
                * that read as "we own no H100s". A free-text affordance cannot
                * add anything here: a type absent from this list is a type we
                * do not hold, and the answer is always no rows.
                */}
              <GpuTypeSelect
                id="match-gpu-type"
                label="GPU type"
                value={form.gpuType}
                onChange={(gpuType) => setForm({ ...form, gpuType })}
                anyLabel={`Any GPU type${gpuTypeOptions.length ? ` (${gpuTypeOptions.length} on the book)` : ''}`}
                placeholder={blocksLoading ? 'Loading the book…' : 'Any GPU type'}
                options={gpuTypeOptions.map((option) => ({
                  gpuType: option.gpuType,
                  hint: `${compactNumber(option.availableGpuHours)} hrs free`,
                }))}
              />
            </FormField>
            <FormField label="GPUs">
              <Input
                id="match-gpu-count"
                value={form.gpuCount}
                onChange={(e) => setForm({ ...form, gpuCount: e.target.value })}
                // A numeric keypad on phones, without the spinner arrows and
                // scroll-to-change behaviour of type="number".
                inputMode="numeric"
                pattern="[0-9]*"
              />
            </FormField>
            <FormField label="GPU-hours">
              <Input
                id="match-gpu-hours"
                value={form.totalGpuHours}
                onChange={(e) => setForm({ ...form, totalGpuHours: e.target.value })}
                inputMode="numeric"
                placeholder="Optional"
              />
            </FormField>
            <FormField label="Max $/GPU-hr">
              <Input
                id="match-max-price"
                value={form.maxPrice}
                onChange={(e) => setForm({ ...form, maxPrice: e.target.value })}
                inputMode="decimal"
                placeholder="Optional"
              />
            </FormField>
            <FormField label="Needed from">
              <Input
                id="match-starts-at"
                type="date"
                value={form.startsAt}
                onChange={(e) => setForm({ ...form, startsAt: e.target.value })}
              />
            </FormField>
            <FormField label="Needed until">
              <Input
                id="match-ends-at"
                type="date"
                value={form.endsAt}
                min={form.startsAt || undefined}
                onChange={(e) => setForm({ ...form, endsAt: e.target.value })}
              />
            </FormField>

            <label className="tap flex items-center gap-2.5 text-sm sm:col-span-2 lg:col-span-3">
              <input
                type="checkbox"
                checked={form.requiresHighSpeedInterconnect}
                onChange={(e) =>
                  setForm({ ...form, requiresHighSpeedInterconnect: e.target.checked })
                }
                className="h-5 w-5 rounded border-border accent-[hsl(var(--accent))]"
              />
              <span>
                Needs high-speed interconnect
                <span className="ml-1 text-muted">— distributed training</span>
              </span>
            </label>

            <Button type="submit" variant="primary" disabled={mutation.isPending} className="min-h-11 w-full lg:col-start-4">
              <Search data-icon="inline-start" aria-hidden />
              {mutation.isPending ? 'Matching…' : 'Find capacity'}
            </Button>
          </form>
        </CardContent>
      </Card>

      {mutation.isSuccess ? (
        mutation.data.length === 0 ? (
          <Card>
            <CardContent className="pt-5">
              <EmptyState
                icon={<Zap className="h-8 w-8" />}
                title="Nothing on the book fits"
                description="No committed capacity matches this requirement. Provider inventory shows what the market is asking for it, which would mean opening a supply deal."
              />
              {/*
                * An empty state that only says "nothing fits" leaves the user
                * guessing which of six constraints did it, and the honest
                * answer is already in the availability rows we hold. Naming the
                * binding constraint turns a dead end into an instruction.
                */}
              <MatchGap
                blocks={blocks ?? []}
                requirement={{
                  gpuType: form.gpuType === ANY_GPU_TYPE ? undefined : form.gpuType,
                  gpuCount: Number(form.gpuCount) || 1,
                  totalGpuHours: form.totalGpuHours ? Number(form.totalGpuHours) : undefined,
                  startsAt: form.startsAt,
                  endsAt: form.endsAt,
                  requiresHighSpeedInterconnect: form.requiresHighSpeedInterconnect,
                }}
              />
            </CardContent>
          </Card>
        ) : (
          <div className="flex flex-col gap-3">
            {mutation.data.map((match) => (
              <Card key={match.commitmentId}>
                <CardContent className="pt-4">
                  <div className="flex flex-wrap items-start justify-between gap-2">
                    <div className="min-w-0">
                      <CardTitle className="min-w-0 break-words text-base leading-snug">
                        {match.name}
                      </CardTitle>
                      <p className="mt-1 text-xs text-muted">
                        {match.gpuCount}× {match.gpuType} · {match.interconnectType} ·{' '}
                        {compactNumber(match.availableGpuHours)} GPU-hrs sellable
                      </p>
                    </div>
                    <Badge tone={match.score > 0.7 ? 'positive' : 'neutral'}>
                      {percent(match.score)} fit
                    </Badge>
                  </div>
                  <ul className="mt-3 flex flex-col gap-1 text-sm">
                    {match.rationale.map((reason, i) => (
                      <li
                        key={i}
                        className={
                          reason.startsWith('⚠') ? 'text-warning' : 'text-muted'
                        }
                      >
                        {reason}
                      </li>
                    ))}
                  </ul>
                  <div className="mt-4 flex flex-col gap-2 border-t border-border pt-3 sm:flex-row sm:items-center sm:justify-between">
                    <p className="text-xs text-muted">
                      {dateRange(match.startsAt, match.endsAt)} · {match.breakEvenPriceCents == null ? 'fully sold' : match.breakEvenPriceCents === 0 ? 'cost covered' : `${unitPrice(match.breakEvenPriceCents)}/GPU-hr break even`}
                    </p>
                    <Button
                      variant="primary"
                      disabled={!writable}
                      onClick={() => onAllocate(
                        match.commitmentId,
                        mutation.data,
                        form.totalGpuHours ? Number(form.totalGpuHours) : undefined,
                      )}
                      title={!writable ? 'Allocating capacity needs demand-team write access' : undefined}
                    >
                      <ShieldCheck data-icon="inline-start" aria-hidden />
                      Allocate this capacity
                    </Button>
                  </div>
                </CardContent>
              </Card>
            ))}
          </div>
        )
      ) : null}

      {mutation.isError ? (
        <div role="alert" className="flex flex-col gap-3 rounded-xl border border-danger/30 bg-danger/10 p-4 text-sm text-danger sm:flex-row sm:items-center sm:justify-between">
          <div className="flex gap-2"><AlertTriangle className="mt-0.5 size-4 shrink-0" aria-hidden /><p>{mutation.error instanceof Error ? mutation.error.message : 'Match failed.'}</p></div>
          <Button variant="outline" onClick={() => mutation.mutate()}>Try again</Button>
        </div>
      ) : null}
    </div>
  );
}

// ------------------------------------------------------------ GPU type control

/**
 * A GPU-type filter that cannot be wrong, because every option came from data.
 *
 * Shared by the matcher and the inventory tab; the hint differs (hours free
 * against listings observed) so it is supplied per option rather than derived.
 */
function GpuTypeSelect({
  id,
  label,
  value,
  onChange,
  options,
  anyLabel,
  placeholder,
}: {
  id: string;
  label: string;
  value: string;
  onChange(value: string): void;
  options: { gpuType: string; hint: string }[];
  anyLabel: string;
  placeholder?: string;
}) {
  return (
    <Select value={value} onValueChange={onChange}>
      <SelectTrigger id={id} aria-label={label}>
        <SelectValue placeholder={placeholder} />
      </SelectTrigger>
      <SelectContent>
        <SelectGroup>
          <SelectItem value={ANY_GPU_TYPE}>{anyLabel}</SelectItem>
          {options.map((option) => (
            <SelectItem key={option.gpuType} value={option.gpuType}>
              {option.gpuType} · {option.hint}
            </SelectItem>
          ))}
        </SelectGroup>
      </SelectContent>
    </Select>
  );
}

/** Distinct GPU types on the book, with how much of each is still sellable. */
function summariseGpuTypes(
  blocks: AvailabilityRow[],
): { gpuType: string; blocks: number; availableGpuHours: number }[] {
  const byType = new Map<string, { gpuType: string; blocks: number; availableGpuHours: number }>();
  for (const block of blocks) {
    const entry = byType.get(block.gpuType) ?? {
      gpuType: block.gpuType,
      blocks: 0,
      availableGpuHours: 0,
    };
    entry.blocks += 1;
    entry.availableGpuHours += block.availableGpuHours;
    byType.set(block.gpuType, entry);
  }
  return [...byType.values()].sort((a, b) => b.availableGpuHours - a.availableGpuHours);
}

// -------------------------------------------------------------- match diagnosis

interface MatchRequirement {
  gpuType?: string;
  gpuCount: number;
  totalGpuHours?: number;
  /** As typed into the date inputs — empty means unconstrained. */
  startsAt: string;
  endsAt: string;
  requiresHighSpeedInterconnect: boolean;
}

function MatchGap({
  blocks,
  requirement,
}: {
  blocks: AvailabilityRow[];
  requirement: MatchRequirement;
}) {
  const reasons = describeMatchGap(blocks, requirement);
  if (reasons.length === 0) return null;

  return (
    <div className="mx-auto max-w-lg rounded-md bg-surface-2 p-4 text-left">
      <Label>What excluded the book</Label>
      <ul className="mt-2 flex flex-col gap-1.5 text-sm text-muted">
        {reasons.map((reason) => (
          <li key={reason}>{reason}</li>
        ))}
      </ul>
    </div>
  );
}

/**
 * Which constraint actually emptied the result, in the order the API applies
 * them.
 *
 * Applied cumulatively rather than independently, because "no block is B200"
 * and "no B200 block runs 512 GPUs" are different sentences and only the first
 * true one is worth saying. Price is deliberately absent: the API scores a
 * requirement's ceiling rather than filtering on it, so it can never be the
 * reason nothing came back.
 */
function describeMatchGap(blocks: AvailabilityRow[], requirement: MatchRequirement): string[] {
  if (blocks.length === 0) return [];
  const reasons: string[] = [];
  let pool = blocks;

  if (requirement.gpuType) {
    const next = pool.filter((block) => block.gpuType === requirement.gpuType);
    if (next.length === 0) {
      const held = [...new Set(pool.map((block) => block.gpuType))].join(', ');
      reasons.push(`No live block is ${requirement.gpuType}. The book holds ${held}.`);
    }
    pool = next;
  }

  if (pool.length > 0) {
    const next = pool.filter((block) => block.gpuCount >= requirement.gpuCount);
    if (next.length === 0) {
      const largest = Math.max(...pool.map((block) => block.gpuCount));
      reasons.push(
        `No block runs ${requirement.gpuCount} GPUs together — the largest is ${largest}.`,
      );
    }
    pool = next;
  }

  if (pool.length > 0 && requirement.requiresHighSpeedInterconnect) {
    const next = pool.filter((block) => FAST_FABRICS.includes(block.interconnectType));
    if (next.length === 0) {
      reasons.push(
        'What is left is Ethernet only. Clear "needs high-speed interconnect" to see it.',
      );
    }
    pool = next;
  }

  if (pool.length > 0 && requirement.startsAt) {
    const wanted = new Date(`${requirement.startsAt}T00:00:00`);
    const next = pool.filter((block) => new Date(block.startsAt) <= wanted);
    if (next.length === 0) {
      const earliest = pool.reduce((soonest, block) =>
        block.startsAt < soonest.startsAt ? block : soonest,
      );
      reasons.push(`Nothing has started by then — the earliest begins ${shortDate(earliest.startsAt)}.`);
    }
    pool = next;
  }

  if (pool.length > 0 && requirement.endsAt) {
    const wanted = new Date(`${requirement.endsAt}T23:59:59`);
    const next = pool.filter((block) => new Date(block.endsAt) >= wanted);
    if (next.length === 0) {
      const latest = pool.reduce((longest, block) =>
        block.endsAt > longest.endsAt ? block : longest,
      );
      reasons.push(`Nothing runs that long — the last block ends ${shortDate(latest.endsAt)}.`);
    }
    pool = next;
  }

  const wantedHours = requirement.totalGpuHours;
  if (pool.length > 0 && wantedHours) {
    const next = pool.filter((block) => block.availableGpuHours >= wantedHours);
    if (next.length === 0) {
      const largest = Math.max(...pool.map((block) => block.availableGpuHours));
      reasons.push(
        `No single block has ${compactNumber(wantedHours)} GPU-hours left — the deepest holds ${compactNumber(largest)}.`,
      );
    }
  }

  return reasons;
}

// ------------------------------------------------------------ provider market

interface InventoryListing {
  id: string;
  accountId: string | null;
  providerSlug: string | null;
  gpuType: string;
  gpuCount: number;
  interconnectType: string;
  region: string | null;
  securityTier: string;
  stockStatus: string;
  isSpot: boolean;
  onDemandPriceCents: number | null;
  communityPriceCents: number | null;
  priceIsVariable: boolean;
  observedAt: string;
}

interface AccountName {
  id: string;
  name: string;
}

/**
 * Past this, a price is a memory rather than a quote.
 *
 * Chosen because provider on-demand pricing moves on a daily cadence, not an
 * hourly one — the failure this prevents is a seller quoting a customer from a
 * number scraped last week and finding out at signature.
 */
const STALE_AFTER_MS = 24 * 3_600_000;

/** A listing's price set against the cheapest block of the same GPU we hold. */
interface Spread {
  block: AvailabilityRow;
  priceCents: number;
  spreadCents: number;
}

function ProviderInventory() {
  const me = useIdentity();
  const [gpuType, setGpuType] = useState(ANY_GPU_TYPE);

  const listings = useQuery({
    queryKey: ['inventory'],
    // Filtering happens in the browser rather than through the endpoint's query
    // parameters, so the GPU-type control is always built from the whole market
    // and cannot offer an option that returns nothing.
    queryFn: () => get<InventoryListing[]>('/api/inventory?limit=200'),
  });
  const availability = useQuery({
    queryKey: ['availability'],
    queryFn: () => get<AvailabilityRow[]>('/api/capacity/availability'),
  });
  /*
   * Provider names come from the accounts each listing is attributed to, which
   * needs `book:read` — a capability this page does not require, since it is
   * gated on `economics:read`. Requesting them regardless would 403 for a
   * finance-only reader and blank the tab, so names are an enhancement and the
   * provider slug stands in when they are not readable.
   */
  const accounts = useQuery({
    queryKey: ['accounts', 'inventory-provider-names'],
    queryFn: () => get<AccountName[]>('/api/accounts'),
    enabled: canAny(me, 'book:read'),
  });

  const blocks = useMemo(() => availability.data ?? [], [availability.data]);
  const rows = useMemo(() => listings.data ?? [], [listings.data]);
  const accountNames = useMemo(
    () => new Map((accounts.data ?? []).map((account) => [account.id, account.name])),
    [accounts.data],
  );

  const typeOptions = useMemo(() => {
    const byType = new Map<string, number>();
    for (const row of rows) byType.set(row.gpuType, (byType.get(row.gpuType) ?? 0) + 1);
    return [...byType.entries()]
      .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
      .map(([type, count]) => ({ gpuType: type, hint: `${count} listing${count === 1 ? '' : 's'}` }));
  }, [rows]);

  const visible = useMemo(
    () => (gpuType === ANY_GPU_TYPE ? rows : rows.filter((row) => row.gpuType === gpuType)),
    [rows, gpuType],
  );
  const groups = useMemo(
    () => groupByProvider(visible, blocks, accountNames),
    [visible, blocks, accountNames],
  );
  const spreads = useMemo(
    () =>
      visible
        .map((row) => spreadAgainstBook(row, blocks))
        .filter((spread): spread is Spread => spread !== null),
    [visible, blocks],
  );
  const staleCount = visible.filter((row) => isStale(row.observedAt)).length;

  if (listings.isLoading) {
    return (
      <div className="grid items-start gap-3 md:grid-cols-2 xl:grid-cols-3">
        {Array.from({ length: 6 }).map((_, index) => (
          <Skeleton key={index} className="h-48" />
        ))}
      </div>
    );
  }

  if (listings.error) {
    return (
      <Card>
        <CardContent className="flex flex-col items-center gap-4 pt-6">
          <EmptyState
            icon={<AlertTriangle className="size-8" />}
            title="Could not load provider inventory"
            description={listings.error instanceof Error ? listings.error.message : 'Inventory is unavailable.'}
          />
          <Button variant="outline" onClick={() => void listings.refetch()}>
            Try again
          </Button>
        </CardContent>
      </Card>
    );
  }

  if (rows.length === 0) {
    return (
      <Card>
        <CardContent className="pt-5">
          <EmptyState
            icon={<Boxes className="h-8 w-8" />}
            title="No provider inventory observed"
            description="Inventory arrives from provider syncs and imports. Once listings land, this view prices them against what the same GPUs cost us."
          />
        </CardContent>
      </Card>
    );
  }

  const medianSpread = median(spreads.map((spread) => spread.spreadCents));

  return (
    <div className="space-y-6">
      <p className="text-sm text-muted">
        What providers are asking, for capacity we do not hold. The number that pays
        the business is the spread: an hour on our book against an hour on theirs.
      </p>

      <div className="grid gap-3 sm:grid-cols-3">
        <Stat
          label="Listings"
          value={compactNumber(visible.length)}
          hint={`${groups.length} provider${groups.length === 1 ? '' : 's'}${gpuType === ANY_GPU_TYPE ? '' : ` · ${gpuType} only`}`}
        />
        <Stat
          label="Median spread"
          value={
            medianSpread == null ? (
              '—'
            ) : (
              <>
                {signedUnitPrice(medianSpread)}
                <span className="text-base font-normal text-muted">/GPU-hr</span>
              </>
            )
          }
          hint={
            medianSpread == null
              ? 'No listing shares a GPU type with a sellable block'
              : `Across ${spreads.length} listing${spreads.length === 1 ? '' : 's'} we could serve from the book`
          }
          tone={medianSpread == null ? 'default' : medianSpread >= 0 ? 'positive' : 'danger'}
        />
        <Stat
          label="Stale quotes"
          value={compactNumber(staleCount)}
          hint={
            staleCount > 0
              ? 'Observed over a day ago — indicative only'
              : 'Every listing seen within the day'
          }
          tone={staleCount > 0 ? 'warning' : 'default'}
        />
      </div>

      <FormField label="GPU type" className="sm:max-w-xs">
        <GpuTypeSelect
          id="inventory-gpu-type"
          label="Filter inventory by GPU type"
          value={gpuType}
          onChange={setGpuType}
          anyLabel={`Every GPU type (${typeOptions.length})`}
          options={typeOptions}
        />
      </FormField>

      {groups.length === 0 ? (
        <Card>
          <CardContent className="pt-5">
            <EmptyState
              icon={<Boxes className="h-8 w-8" />}
              title={`No ${gpuType} listings`}
              description="Nothing in the observed market matches this filter."
              action={
                <Button variant="outline" onClick={() => setGpuType(ANY_GPU_TYPE)}>
                  Show every GPU type
                </Button>
              }
            />
          </CardContent>
        </Card>
      ) : (
        /* `items-start` so a provider with two listings does not stretch to the
           height of one with six, which reads as an empty half-card. */
        <div className="grid items-start gap-3 md:grid-cols-2 xl:grid-cols-3">
          {groups.map((group) => (
            <Card key={group.key} className="min-w-0">
              <CardHeader>
                <div className="flex items-start justify-between gap-2">
                  <CardTitle className="min-w-0 break-words text-base leading-snug">
                    {group.name}
                  </CardTitle>
                  {group.bestSpreadCents == null ? null : (
                    <Badge
                      tone={group.bestSpreadCents >= 0 ? 'positive' : 'danger'}
                      title="Widest spread over our cost across this provider's listings"
                      // A figure that wraps mid-number stops being a figure; the
                      // long provider name is the part that should give way.
                      className="shrink-0 whitespace-nowrap"
                    >
                      {signedUnitPrice(group.bestSpreadCents)} best
                    </Badge>
                  )}
                </div>
                <p className="text-xs text-muted">
                  {group.listings.length} listing{group.listings.length === 1 ? '' : 's'} · seen{' '}
                  {relativeTime(group.freshestAt)}
                </p>
              </CardHeader>
              <CardContent>
                <ul className="flex flex-col">
                  {group.listings.map(({ listing, spread }) => (
                    <InventoryRow key={listing.id} listing={listing} spread={spread} />
                  ))}
                </ul>
              </CardContent>
            </Card>
          ))}
        </div>
      )}
    </div>
  );
}

function InventoryRow({ listing, spread }: { listing: InventoryListing; spread: Spread | null }) {
  const price = listingPriceCents(listing);
  const stale = isStale(listing.observedAt);

  /*
   * The row is deliberately not `flex-wrap`: a wrapped price column lands under
   * the badges and reads as a caption rather than a figure. Both columns shrink
   * instead — the badges and the spread wrap within their own column — which is
   * what keeps a phone from scrolling sideways.
   */
  return (
    <li className="flex items-start justify-between gap-3 border-t border-border/70 py-2.5 first:border-t-0 first:pt-0 last:pb-0">
      <div className="min-w-0 flex-1">
        <p className="text-sm font-medium">
          {listing.gpuCount}× {listing.gpuType}
        </p>
        <p className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-muted">
          <span>{[listing.region, listing.interconnectType].filter(Boolean).join(' · ')}</span>
          {listing.securityTier === 'community_cloud' ? <Badge>Community</Badge> : null}
          {listing.isSpot ? <Badge tone="warning">Spot</Badge> : null}
          {listing.stockStatus === 'Low' ? <Badge tone="warning">Low stock</Badge> : null}
        </p>
        {/*
          * Staleness is stated on every row, not only the stale ones. A price
          * with no age beside it invites the reader to assume it is current,
          * which for half of this book it is not.
          */}
        <p
          className={`mt-1 text-xs ${stale ? 'text-warning' : 'text-muted'}`}
          title={new Date(listing.observedAt).toLocaleString()}
        >
          {stale ? 'Last observed ' : 'Observed '}
          {relativeTime(listing.observedAt)}
          {listing.priceIsVariable ? ' · price varies' : ''}
        </p>
      </div>
      <div className="min-w-0 text-right">
        <p className="nums whitespace-nowrap text-sm font-semibold">
          {price == null ? 'Not published' : unitPrice(price)}
          {price == null ? null : <span className="text-xs font-normal text-muted">/GPU-hr</span>}
        </p>
        {spread ? (
          <p
            className={`nums mt-0.5 text-xs ${spread.spreadCents >= 0 ? 'text-positive' : 'text-danger'}`}
            title={`Cheapest sellable ${listing.gpuType} block: ${spread.block.name}`}
          >
            {signedUnitPrice(spread.spreadCents)} on our{' '}
            {unitPrice(spread.block.costPerGpuHourCents)} cost
          </p>
        ) : (
          <p className="mt-0.5 text-xs text-muted">No sellable block to compare</p>
        )}
        {listing.communityPriceCents != null &&
        listing.communityPriceCents !== listing.onDemandPriceCents ? (
          <p className="nums mt-0.5 text-xs text-muted">
            {unitPrice(listing.communityPriceCents)} community
          </p>
        ) : null}
      </div>
    </li>
  );
}

/**
 * The headline quote. The community price, where a provider publishes one, is
 * shown beneath rather than substituted: they price different things, and
 * silently taking the lower of the two would flatter every spread on the page.
 */
function listingPriceCents(listing: InventoryListing): number | null {
  return listing.onDemandPriceCents ?? listing.communityPriceCents;
}

function isStale(observedAt: string): boolean {
  const observed = new Date(observedAt).getTime();
  return Number.isFinite(observed) && Date.now() - observed > STALE_AFTER_MS;
}

/**
 * A spread, which is the difference between two $/GPU-hr figures and so is one
 * itself — `unitPrice`, never `money`. A spread of exactly $1 printed as "+$1"
 * beside a cost of "$1.89" reads as two different kinds of number.
 */
function signedUnitPrice(cents: number): string {
  return cents > 0 ? `+${unitPrice(cents)}` : unitPrice(cents);
}

/**
 * A listing priced against the cheapest block of the same GPU we could actually
 * serve from.
 *
 * Cheapest, because the marginal hour comes off whichever block costs least;
 * and only blocks with hours left, because a fully sold commitment cannot serve
 * anyone and comparing against its cost would invent a margin we cannot take.
 */
function spreadAgainstBook(listing: InventoryListing, blocks: AvailabilityRow[]): Spread | null {
  const priceCents = listingPriceCents(listing);
  if (priceCents == null) return null;

  let block: AvailabilityRow | undefined;
  for (const candidate of blocks) {
    if (candidate.gpuType !== listing.gpuType || candidate.availableGpuHours <= 0) continue;
    if (!block || candidate.costPerGpuHourCents < block.costPerGpuHourCents) block = candidate;
  }
  if (!block) return null;

  return { block, priceCents, spreadCents: priceCents - block.costPerGpuHourCents };
}

interface ProviderGroup {
  key: string;
  name: string;
  listings: { listing: InventoryListing; spread: Spread | null }[];
  freshestAt: string;
  bestSpreadCents: number | null;
}

/**
 * Listings gathered by provider, widest spread first.
 *
 * Ordering by spread rather than alphabetically because the question this tab
 * answers is "who is the market paying most over our cost", and that provider
 * should not be three cards down the page.
 */
function groupByProvider(
  listings: InventoryListing[],
  blocks: AvailabilityRow[],
  accountNames: Map<string, string>,
): ProviderGroup[] {
  const groups = new Map<string, ProviderGroup>();

  for (const listing of listings) {
    const key = listing.providerSlug ?? listing.accountId ?? listing.id;
    const spread = spreadAgainstBook(listing, blocks);
    const group = groups.get(key) ?? {
      key,
      name: providerName(listing, accountNames),
      listings: [],
      freshestAt: listing.observedAt,
      bestSpreadCents: null,
    };
    group.listings.push({ listing, spread });
    if (listing.observedAt > group.freshestAt) group.freshestAt = listing.observedAt;
    if (spread && (group.bestSpreadCents == null || spread.spreadCents > group.bestSpreadCents)) {
      group.bestSpreadCents = spread.spreadCents;
    }
    groups.set(key, group);
  }

  for (const group of groups.values()) {
    group.listings.sort(
      (a, b) =>
        (b.spread?.spreadCents ?? Number.NEGATIVE_INFINITY) -
        (a.spread?.spreadCents ?? Number.NEGATIVE_INFINITY),
    );
  }

  return [...groups.values()].sort(
    (a, b) =>
      (b.bestSpreadCents ?? Number.NEGATIVE_INFINITY) -
        (a.bestSpreadCents ?? Number.NEGATIVE_INFINITY) || a.name.localeCompare(b.name),
  );
}

/**
 * The account's own name where the reader may see accounts, else the slug.
 * Title-casing the slug is a last resort: it renders "Datacrunch" for a company
 * that writes itself DataCrunch, which the account record gets right.
 */
function providerName(listing: InventoryListing, accountNames: Map<string, string>): string {
  const named = listing.accountId ? accountNames.get(listing.accountId) : undefined;
  if (named) return named;
  if (!listing.providerSlug) return 'Unattributed provider';
  return listing.providerSlug
    .split(/[-_]/)
    .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
    .join(' ');
}

function median(values: number[]): number | null {
  if (values.length === 0) return null;
  const sorted = [...values].sort((a, b) => a - b);
  const middle = Math.floor(sorted.length / 2);
  const upper = sorted[middle];
  if (upper == null) return null;
  if (sorted.length % 2 === 1) return upper;
  const lower = sorted[middle - 1];
  return lower == null ? upper : Math.round((lower + upper) / 2);
}
