/**
 * Capacity — availability, and the matcher.
 *
 * 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.
 */
import { useState } from 'react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { AlertTriangle, Lock, Search, Server, ShieldCheck, Zap } from 'lucide-react';
import { compactNumber, get, money, percent, post, shortDate } from '@/lib/api';
import { usePageTitle } from '@/lib/title';
import { useIdentity } from '@/lib/identity';
import { can, canAny } from '@/lib/permissions';
import {
  AllocationSheet,
  type AvailabilityRow,
  type MatchRow,
} from '@/components/AllocationSheet';
import {
  Badge,
  Button,
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  EmptyState,
  Input,
  Skeleton,
} from '@/components/ui';

export function Capacity() {
  usePageTitle('Capacity');
  const [tab, setTab] = useState<'available' | 'match'>('available');
  const [allocation, setAllocation] = useState<{
    open: boolean;
    preferredCommitmentId?: string;
    matches?: MatchRow[];
    defaultGpuHours?: number;
  }>({ open: false });
  const me = useIdentity();
  /*
   * Two 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 separate: the allocate and hold buttons post to
   * `/api/allocations`, which the server authorises on `deal:write` for the
   * demand team. That is what the button must ask for — not `commitment:write`,
   * which governs `/api/commitments` and is not reachable from this page.
   *
   * `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');

  if (!readable) {
    return (
      <div className="space-y-5">
        <header>
          <h1 className="text-xl font-semibold tracking-tight sm:text-2xl">Capacity</h1>
        </header>
        <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-5 pb-[calc(5.5rem+var(--safe-bottom))] md:pb-0">
      <header>
        <h1 className="text-xl font-semibold tracking-tight sm:text-2xl">Capacity</h1>
        <p className="mt-1 text-sm text-muted">
          What we hold, what is sold, and what is still sellable.
        </p>
      </header>

      {/* 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"
      >
        {(['available', 'match'] as const).map((value) => (
          <button
            key={value}
            role="tab"
            aria-selected={tab === value}
            onClick={() => setTab(value)}
            className={[
              'tap flex-1 rounded-md px-4 text-sm font-medium transition-colors sm:flex-none',
              tab === value ? 'bg-surface text-fg shadow-sm' : 'text-muted',
            ].join(' ')}
          >
            {value === 'available' ? 'Availability' : 'Match a requirement'}
          </button>
        ))}
      </div>

      {tab === 'available' ? (
        <Availability
          writable={writable}
          onAllocate={(preferredCommitmentId) =>
            setAllocation({ open: true, preferredCommitmentId })
          }
        />
      ) : (
        <Matcher
          writable={writable}
          onAllocate={(preferredCommitmentId, matches, defaultGpuHours) =>
            setAllocation({ open: true, preferredCommitmentId, matches, defaultGpuHours })
          }
        />
      )}
      <AllocationSheet
        {...allocation}
        onOpenChange={(open) => setAllocation((state) => ({ ...state, open }))}
      />
    </div>
  );
}

function Availability({ writable, onAllocate }: { writable: boolean; 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="Once you record what capacity you have committed to buy, this view shows how much of each block is sold, held, and still available."
          />
        </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>
        <p className="text-xs text-muted">
          {row.gpuCount}× {row.gpuType} · {row.interconnectType} ·{' '}
          {shortDate(row.startsAt)}–{shortDate(row.endsAt)}
        </p>
      </CardHeader>
      <CardContent className="space-y-3">
        {/* Sold and held are shown as separate segments, because a full-looking
            bar made mostly of unconverted holds is a lie a seller would act on. */}
        <div>
          <div className="flex h-2 overflow-hidden rounded-full bg-surface-2" role="img" aria-label={`${percent(soldPct)} sold, ${percent(heldPct)} held, ${compactNumber(row.availableGpuHours)} GPU-hours sellable`}>
            <div className="bg-primary" style={{ width: `${Math.min(100, soldPct * 100)}%` }} />
            <div
              className="bg-primary/35"
              style={{ width: `${Math.min(100 - soldPct * 100, heldPct * 100)}%` }}
            />
          </div>
          <div className="mt-1.5 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 gap-x-3 gap-y-1.5 text-xs">
          <dt className="text-muted">Cost</dt>
          <dd className="nums text-right">{money(row.costPerGpuHourCents)}/GPU-hr</dd>
          <dt className="text-muted">Break even</dt>
          <dd className="nums text-right">
            {row.breakEvenPriceCents == null
              ? 'Fully sold'
              : row.breakEvenPriceCents === 0
                ? 'Cost covered'
                : `${money(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: '',
    gpuCount: '64',
    totalGpuHours: '',
    startsAt: '',
    endsAt: '',
    requiresHighSpeedInterconnect: true,
    maxPrice: '',
  });

  const mutation = useMutation({
    mutationFn: () =>
      post<MatchRow[]>('/api/capacity/match', {
        gpuType: form.gpuType || undefined,
        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-4">
      <Card>
        <CardHeader>
          <CardTitle className="text-base">What does the customer need?</CardTitle>
          <p className="text-xs leading-relaxed 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-4 sm:grid-cols-2 lg:grid-cols-4"
            aria-label="Capacity requirement"
            onSubmit={(event) => {
              event.preventDefault();
              mutation.mutate();
            }}
          >
            <Field label="GPU type" htmlFor="match-gpu-type">
              <Input
                id="match-gpu-type"
                value={form.gpuType}
                onChange={(e) => setForm({ ...form, gpuType: e.target.value })}
                placeholder="H100_80GB"
                // Hardware identifiers are case-sensitive upstream; correcting
                // them for the user would produce silent mismatches.
                autoCapitalize="off"
                autoCorrect="off"
                spellCheck={false}
              />
            </Field>
            <Field label="GPUs" htmlFor="match-gpu-count">
              <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]*"
              />
            </Field>
            <Field label="GPU-hours" htmlFor="match-gpu-hours">
              <Input
                id="match-gpu-hours"
                value={form.totalGpuHours}
                onChange={(e) => setForm({ ...form, totalGpuHours: e.target.value })}
                inputMode="numeric"
                placeholder="Optional"
              />
            </Field>
            <Field label="Max $/GPU-hr" htmlFor="match-max-price">
              <Input
                id="match-max-price"
                value={form.maxPrice}
                onChange={(e) => setForm({ ...form, maxPrice: e.target.value })}
                inputMode="decimal"
                placeholder="Optional"
              />
            </Field>
            <Field label="Needed from" htmlFor="match-starts-at">
              <Input
                id="match-starts-at"
                type="date"
                value={form.startsAt}
                onChange={(e) => setForm({ ...form, startsAt: e.target.value })}
              />
            </Field>
            <Field label="Needed until" htmlFor="match-ends-at">
              <Input
                id="match-ends-at"
                type="date"
                value={form.endsAt}
                min={form.startsAt || undefined}
                onChange={(e) => setForm({ ...form, endsAt: e.target.value })}
              />
            </Field>

            <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. Search provider inventory to find capacity to buy, which would mean opening a supply deal."
              />
            </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">
                      <p className="font-medium">{match.name}</p>
                      <p className="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">
                      {shortDate(match.startsAt)}–{shortDate(match.endsAt)} · {match.breakEvenPriceCents == null ? 'fully sold' : match.breakEvenPriceCents === 0 ? 'cost covered' : `${money(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>
  );
}

function Field({ label, htmlFor, children }: { label: string; htmlFor: string; children: React.ReactNode }) {
  return (
    <label className="block" htmlFor={htmlFor}>
      <span className="mb-1 block text-xs font-medium text-muted">{label}</span>
      {children}
    </label>
  );
}
