/**
 * Margin — the ledger, per block and in total.
 *
 * The table scrolls inside its own pane on narrow screens rather than making
 * the page scroll sideways; a card list would lose the column comparison that
 * is the entire value of this view.
 *
 * The page also has to say the quiet part out loud. A blended margin in the low
 * single digits reads as a thin but healthy book, while one commitment sits
 * barely half sold and has not paid for itself — the totals average that away
 * by construction. So the blocks whose cost is still uncovered are named above
 * the table with the price their remaining hours have to fetch, rather than
 * left to be reconstructed by reading a percentage column against a price
 * column two columns away.
 */
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { AlertTriangle, ArrowRight } from 'lucide-react';
import { Link } from 'react-router-dom';
import { compactNumber, get, money, percent, unitPrice } from '@/lib/api';
import { PiggyAskButton } from '@/components/PiggyChat';
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, EmptyState, Skeleton, Stat, cn } from '@/components/ui';
import { usePageTitle } from '@/lib/title';
import { usePiggyContext } from '@/lib/piggy-context';

interface MarginReport {
  totals: {
    committedGpuHours: number;
    allocatedGpuHours: number;
    idleGpuHours: number;
    utilisation: number;
    costCents: number;
    revenueCents: number;
    grossMarginCents: number;
    grossMarginPct: number | null;
    marginPerAllocatedGpuHourCents: number | null;
  };
  blocks: MarginBlock[];
}

interface MarginBlock {
  commitmentId: string;
  name: string;
  gpuType: string;
  gpuCount: number;
  totalGpuHours: number;
  soldGpuHours: number;
  availableGpuHours: number;
  costPerGpuHourCents: number;
  utilisation: number;
  breakEvenPriceCents: number | null;
}

/**
 * A block has covered its cost when there is nothing left to break even on.
 *
 * The colour on the sold-ratio column used to key off `utilisation < 0.5`,
 * which is an arbitrary line: the block this page exists to flag is 55% sold
 * and would have rendered as unremarkable, while a block 40% sold on cheap
 * hours it has already earned back would have rendered as a problem. Break-even
 * is the honest test — it is zero exactly when revenue has already covered the
 * whole commitment, and null when there is nothing left to sell.
 */
function isUncovered(block: MarginBlock): boolean {
  return block.breakEvenPriceCents != null && block.breakEvenPriceCents > 0;
}

export function Margin() {
  usePageTitle('Margin');
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ['margin'],
    queryFn: () => get<MarginReport>('/api/capacity/margin'),
  });
  const [focusedId, setFocusedId] = useState<string | null>(null);
  const blocks = data?.blocks ?? [];
  // Resolved from the current data rather than held in state, so a block that
  // disappears on a refetch quietly returns Piggy to the page instead of
  // leaving it pointed at a commitment nobody can see any more.
  const focused = blocks.find((block) => block.commitmentId === focusedId);

  // Ambient context for the dock: the block the user selected, else this page.
  // Published before the early returns below, because a hook that runs only on
  // the happy path is a hook that changes order the first time the query fails.
  usePiggyContext(
    focused
      ? { type: 'commitment', id: focused.commitmentId, label: focused.name }
      : { type: 'page', route: '/margin', label: 'Margin' },
  );

  const uncovered = blocks.filter(isUncovered);
  const toggleFocus = (commitmentId: string) =>
    setFocusedId((current) => (current === commitmentId ? null : commitmentId));

  if (isLoading) {
    return <div className="flex flex-col gap-4"><Skeleton className="h-16" /><div className="grid grid-cols-2 gap-2 xl:grid-cols-4">{Array.from({ length: 4 }).map((_, index) => <Skeleton key={index} className="h-28" />)}</div><Skeleton className="h-80" /></div>;
  }
  if (error || !data) {
    return (
      <Card>
        <CardContent className="flex flex-col items-center gap-4 pt-6">
          <EmptyState title="Could not load margin" description={error instanceof Error ? error.message : 'The margin ledger is unavailable.'} />
          <Button variant="outline" onClick={() => void refetch()}>Try again</Button>
        </CardContent>
      </Card>
    );
  }
  if (data.blocks.length === 0) {
    return (
      <EmptyState
        title="No capacity to report on"
        description="Margin is computed from capacity commitments and the allocations against them. Both are recorded on the capacity book."
        /*
         * A link rather than the commitment sheet itself, which is the opposite
         * of the choice Overview makes and for a reason. Margin is a derived
         * ledger with no other write path on it, and recording a block is only
         * step one — the seller's next move is to match and allocate it, which
         * is on /capacity too. Sending the reader there puts them in front of
         * the whole job rather than dropping a sheet onto a report and
         * returning them to a page that still says nothing sold. It also keeps
         * the capability question in one place: the button on /capacity states
         * whose authority this is, so it is not restated here.
         */
        action={
          <Button variant="primary" asChild>
            <Link to="/capacity">
              Go to the capacity book
              <ArrowRight className="size-4" aria-hidden />
            </Link>
          </Button>
        }
      />
    );
  }

  const t = data.totals;

  return (
    <div className="space-y-5 pb-[calc(5.5rem+var(--safe-bottom))] md:pb-0">
      <header className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
        <div className="min-w-0">
          <h1 className="text-xl font-semibold tracking-tight sm:text-2xl">Margin</h1>
          <p className="mt-1 max-w-2xl text-sm text-muted">
            Revenue from what we sold, against the full cost of what we bought. Every
            commitment is charged in full, so a block keeps paying for the hours nobody
            has bought yet.
          </p>
        </div>
        {/*
          No `context` prop, deliberately. This button asks about whatever the
          page has published — the selected block, else /margin — and pinning it
          to the page here would make selecting a block change the dock and not
          this button, which is the one the user just pressed.
        */}
        <PiggyAskButton
          label={focused ? 'Ask about this block' : 'Ask Piggy'}
          prompt={
            focused
              ? 'How much of this block is still unsold, what must the rest fetch to cover it, and how much term is left to sell into?'
              : 'Which commitment is furthest from covering its cost, and what would the remaining hours have to fetch?'
          }
        />
      </header>

      <section className="grid grid-cols-2 gap-2 sm:gap-3 xl:grid-cols-4">
        <Stat label="Revenue" value={money(t.revenueCents)} hint={`${compactNumber(t.allocatedGpuHours)} GPU-hrs sold`} />
        <Stat label="Cost" value={money(t.costCents)} hint={`${compactNumber(t.committedGpuHours)} GPU-hrs committed`} />
        <Stat
          label="Gross margin"
          value={money(t.grossMarginCents)}
          hint={`${percent(t.grossMarginPct, 1)} of revenue, after the idle hours`}
          tone={t.grossMarginCents >= 0 ? 'positive' : 'danger'}
        />
        <Stat
          label="Per sold GPU-hour"
          value={unitPrice(t.marginPerAllocatedGpuHourCents)}
          hint={`${percent(t.utilisation, 1)} of committed hours sold`}
        />
      </section>

      {uncovered.length > 0 ? (
        <Card className="border-warning/30">
          <CardHeader className="space-y-0">
            <div className="flex items-start gap-2">
              <AlertTriangle className="mt-0.5 size-4 shrink-0 text-warning" aria-hidden />
              <div className="min-w-0">
                <CardTitle className="text-base">
                  {uncovered.length} of {data.blocks.length} commitments have not covered their cost
                </CardTitle>
                {/*
                  Not "an average of the blocks": the blended figure is a ratio
                  of sums, and describing it as an average invites exactly the
                  per-block averaging `aggregateMargin` refuses to do.
                */}
                <p className="mt-1 text-sm text-muted">
                  {t.grossMarginPct == null
                    ? 'Nothing has sold yet, so every commitment below is still owed its whole cost.'
                    : `The book clears ${percent(t.grossMarginPct, 1)} blended because the blocks that have earned their money back carry the ones that have not.`}{' '}
                  These are the blocks still owed something, and the price the rest of each has to
                  fetch to get there.
                </p>
              </div>
            </div>
          </CardHeader>
          <CardContent className="space-y-2">
            {uncovered.map((block) => (
              <div key={block.commitmentId} className="flex flex-col gap-2 rounded-lg bg-surface-2 p-3 sm:flex-row sm:items-center sm:justify-between">
                <div className="min-w-0">
                  <p className="break-words font-medium leading-snug">{block.name}</p>
                  <p className="mt-1 text-xs text-muted">
                    {block.gpuCount}× {block.gpuType} · <span className="nums">{percent(block.utilisation)}</span> sold ·{' '}
                    <span className="nums">{compactNumber(block.availableGpuHours)}</span> hrs still sellable
                  </p>
                </div>
                <p className="shrink-0 text-sm sm:text-right">
                  <span className="nums font-semibold text-warning">{unitPrice(block.breakEvenPriceCents)}</span>
                  <span className="text-muted">/GPU-hr to break even</span>
                  {/*
                    Break-even sits below cost once part of the block has sold —
                    the hours already invoiced have paid down some of it. Said
                    here because the two prices are otherwise read as a
                    contradiction rather than as progress.
                  */}
                  <span className="mt-0.5 block text-xs text-muted">
                    against <span className="nums">{unitPrice(block.costPerGpuHourCents)}</span>/GPU-hr paid
                  </span>
                </p>
              </div>
            ))}
          </CardContent>
        </Card>
      ) : null}

      <Card>
        <CardHeader className="flex-row items-center justify-between gap-3 space-y-0">
          <div>
            <CardTitle className="text-base">By commitment</CardTitle>
            <p className="mt-1 text-xs text-muted">
              Sold ratio describes contracted capacity sold, not workload utilization. Select a
              commitment to point Piggy at it.
            </p>
          </div>
          <Link to="/capacity" className="tap inline-flex min-h-11 shrink-0 items-center gap-1 rounded-lg px-3 text-sm font-medium text-accent-fg hover:bg-surface-2">
            Capacity <ArrowRight className="size-4" aria-hidden />
          </Link>
        </CardHeader>
        <CardContent className="px-0 sm:px-0">
          <div className="hidden scroll-x md:block">
            <table className="w-full min-w-[720px] text-sm">
              <thead>
                <tr className="border-b border-border text-left text-xs uppercase tracking-wide text-muted">
                  <th className="px-4 pb-2 font-medium sm:px-5">Commitment</th>
                  <th className="px-4 pb-2 text-right font-medium">Sold</th>
                  <th className="px-4 pb-2 text-right font-medium">Sellable</th>
                  <th className="px-4 pb-2 text-right font-medium">Sold ratio</th>
                  <th className="px-4 pb-2 text-right font-medium">Cost/hr</th>
                  <th className="px-4 pb-2 text-right font-medium sm:px-5">Break even</th>
                </tr>
              </thead>
              <tbody>
                {data.blocks.map((block) => (
                  <tr
                    key={block.commitmentId}
                    aria-selected={block.commitmentId === focusedId}
                    className={cn(
                      'border-b border-border/60 last:border-0',
                      block.commitmentId === focusedId && 'bg-surface-2',
                    )}
                  >
                    <td className="px-4 py-3 sm:px-5">
                      <FocusButton
                        block={block}
                        focused={block.commitmentId === focusedId}
                        onToggle={() => toggleFocus(block.commitmentId)}
                      />
                    </td>
                    <td className="nums px-4 py-3 text-right">
                      {compactNumber(block.soldGpuHours)}
                    </td>
                    <td className="nums px-4 py-3 text-right">
                      {compactNumber(block.availableGpuHours)}
                    </td>
                    <td
                      className={[
                        'nums px-4 py-3 text-right font-medium',
                        isUncovered(block) ? 'text-warning' : '',
                      ].join(' ')}
                    >
                      {percent(block.utilisation)}
                    </td>
                    <td className="nums px-4 py-3 text-right">
                      {unitPrice(block.costPerGpuHourCents)}
                    </td>
                    {/*
                      A zero break-even means the block's cost is already
                      covered, so any further sale is upside. Printing "$0.00"
                      is technically true and reads like a bug — the same fix
                      already applied on the capacity cards.
                    */}
                    <td className="px-4 py-3 text-right sm:px-5"><BreakEven value={block.breakEvenPriceCents} /></td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
          <div className="grid gap-3 px-4 pb-4 md:hidden">
            {data.blocks.map((block) => (
              <article
                key={block.commitmentId}
                className={cn(
                  'rounded-xl border border-border p-4',
                  // `border-brand`, not `border-accent`: `accent` is shadcn's
                  // subtle surface in this config, so a border in it disappears.
                  block.commitmentId === focusedId && 'border-brand bg-surface-2',
                )}
              >
                <div className="flex items-start justify-between gap-3">
                  <FocusButton
                    block={block}
                    focused={block.commitmentId === focusedId}
                    onToggle={() => toggleFocus(block.commitmentId)}
                  />
                  <span className={['nums shrink-0 text-sm font-semibold', isUncovered(block) ? 'text-warning' : ''].join(' ')}>{percent(block.utilisation)} sold</span>
                </div>
                <dl className="mt-4 grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
                  <dt className="text-muted">Sold capacity</dt><dd className="nums text-right">{compactNumber(block.soldGpuHours)} hrs</dd>
                  <dt className="text-muted">Sellable capacity</dt><dd className="nums text-right">{compactNumber(block.availableGpuHours)} hrs</dd>
                  <dt className="text-muted">Our cost</dt><dd className="nums text-right">{unitPrice(block.costPerGpuHourCents)}/GPU-hr</dd>
                  <dt className="text-muted">Break even</dt><dd className="text-right"><BreakEven value={block.breakEvenPriceCents} /></dd>
                </dl>
              </article>
            ))}
          </div>
        </CardContent>
      </Card>
    </div>
  );
}

/**
 * The commitment name, as the control that points Piggy at that commitment.
 *
 * A row is the thing a reader is already looking at when they want to ask about
 * it, so the name carries the selection rather than a separate button in a
 * seventh column that would push the table wider than the pane it scrolls in.
 */
function FocusButton({ block, focused, onToggle }: { block: MarginBlock; focused: boolean; onToggle(): void }) {
  return (
    <button
      type="button"
      aria-pressed={focused}
      onClick={onToggle}
      title={focused ? 'Stop pointing Piggy at this commitment' : 'Point Piggy at this commitment'}
      className="tap -mx-2 block min-w-0 rounded-lg px-2 py-1 text-left transition-colors hover:bg-surface-2"
    >
      <span className="block break-words font-medium leading-snug">
        {block.name}
        {focused ? <Badge tone="accent" className="ml-2 align-middle">In focus</Badge> : null}
      </span>
      <span className="mt-0.5 block text-xs text-muted">
        {block.gpuCount}× {block.gpuType}
      </span>
      <span className="sr-only">{focused ? 'Piggy is looking at this commitment' : 'Point Piggy at this commitment'}</span>
    </button>
  );
}

function BreakEven({ value }: { value: number | null }) {
  if (value == null) return <span className="text-muted">Sold out</span>;
  if (value === 0) return <span className="text-positive">Cost covered</span>;
  return <span className="nums">{unitPrice(value)}/GPU-hr</span>;
}
