// #builds — Task 5 of docs/superpowers/plans/2026-08-03-inventory.md. Pick a
// finished item and how many, see exactly what comes off the shelf and what it
// works out to, then build.
//
// EVERY NUMBER ON THE PREVIEW COMES FROM THE SERVER. POST /api/c/builds/preview
// calls build_preview() (0023), the same function post_build() draws against,
// so the sufficiency check, the component draw and the resulting unit cost on
// this screen are the ones the build will actually use — not a second
// implementation that can drift. The only thing computed here is "short by",
// which is qtyNeeded − qtyOnHand with exact integer arithmetic
// (items/quantityMath.ts) purely so the shortfall reads as one number instead
// of two the user has to subtract in their head.
//
// INSUFFICIENT STOCK IS OBVIOUS BEFORE THE BUTTON, and the button is disabled
// with the reason on it. The preview's `blockedBecause` is post_build()'s own
// refusal, restated ahead of the click (server/src/builds.ts) — the database
// stays the guard (CLAUDE.md §12); this is what stops a user meeting it as a
// red banner after committing to the action.
//
// Undoing a build writes compensating movements and a reversing entry
// (reverse_build, 0023). It REFUSES when the finished units are gone — sold,
// or built into something else — because putting the parts back would mean
// un-selling what is already on a customer's invoice. That refusal names the
// build, the item and both numbers, and is surfaced verbatim.

import { useCallback, useEffect, useMemo, useState } from "react";
import { useCompany } from "../lib/company";
import { useApiGet } from "../lib/useApiGet";
import { apiPost, ApiError } from "../lib/api";
import { fmtMoney } from "../lib/money";
import { fmtDate, todayIso } from "../lib/date";
import { ErrorBanner } from "../components/ErrorBanner";
import { SearchablePicker, type PickerGroup } from "../components/SearchablePicker";
import { fmtQuantity, fmtUnitCost, type ApiItem } from "../items/itemTypes";
import { exactUnitCost, hasSubCentDigits, subtractQuantity } from "../items/quantityMath";
import type { NavTarget } from "../lib/useHashView";
import {
  BUILD_COST_NOTE,
  type ApiBuildPreview, type ApiBuildResult, type ApiBuildsBody,
} from "./buildTypes";

const QUANTITY_RE = /^(0|[1-9]\d{0,7})(\.\d{1,4})?$/;

function quantityValid(raw: string): boolean {
  const q = raw.trim();
  return QUANTITY_RE.test(q) && /[1-9]/.test(q);
}

export function BuildsScreen({ navigate }: { navigate: (target: NavTarget) => void }) {
  const { selectedCompanyId } = useCompany();
  const { data: itemsBody, retry: retryItems } = useApiGet<{ items: ApiItem[] }>(
    "/api/c/items", selectedCompanyId,
  );
  const items = useMemo(() => itemsBody?.items ?? [], [itemsBody]);

  const [assemblyItemId, setAssemblyItemId] = useState<string | null>(null);
  const [qty, setQty] = useState("1");
  const [builtOn, setBuiltOn] = useState(todayIso());
  const [memo, setMemo] = useState("");

  const [preview, setPreview] = useState<ApiBuildPreview | null>(null);
  const [previewError, setPreviewError] = useState<string | null>(null);
  const [previewing, setPreviewing] = useState(false);

  const [building, setBuilding] = useState(false);
  const [buildError, setBuildError] = useState<string | null>(null);
  const [built, setBuilt] = useState<ApiBuildResult | null>(null);

  // Bumped after anything that changes what is on the shelf, so the preview
  // (a POST, so not a useApiGet path) refetches alongside the lists.
  const [refreshToken, setRefreshToken] = useState(0);
  const { data: history, loading: historyLoading, error: historyError, retry: retryHistory } =
    useApiGet<ApiBuildsBody>("/api/c/builds?limit=50", selectedCompanyId);

  const stockedGroups: PickerGroup[] = useMemo(() => [{
    label: "Stocked items",
    options: items
      .filter((i) => i.kind === "inventory")
      .map((i) => ({ id: i.id, label: i.sku ? `${i.name} (${i.sku})` : i.name })),
  }], [items]);

  const qtyOk = quantityValid(qty);

  // The preview is the whole point of this screen, so it refetches on every
  // change to what is being built — no "Preview" button to forget to press.
  useEffect(() => {
    if (!selectedCompanyId || assemblyItemId === null || !qtyOk) {
      setPreview(null);
      setPreviewError(null);
      return;
    }
    let cancelled = false;
    setPreviewing(true);
    apiPost<ApiBuildPreview>(
      "/api/c/builds/preview",
      { assemblyItemId, qty: qty.trim() },
      selectedCompanyId,
    )
      .then((body) => {
        if (!cancelled) {
          setPreview(body);
          setPreviewError(null);
        }
      })
      .catch((err: unknown) => {
        if (!cancelled) {
          setPreview(null);
          setPreviewError(
            err instanceof ApiError ? err.message : "Couldn't work out what this build would draw.",
          );
        }
      })
      .finally(() => {
        if (!cancelled) setPreviewing(false);
      });
    return () => { cancelled = true; };
  }, [selectedCompanyId, assemblyItemId, qty, qtyOk, refreshToken]);

  const refreshAll = useCallback(() => {
    setRefreshToken((n) => n + 1);
    retryItems();
    retryHistory();
  }, [retryItems, retryHistory]);

  async function build() {
    if (!selectedCompanyId || assemblyItemId === null || !qtyOk || builtOn === "") return;
    setBuilding(true);
    setBuildError(null);
    try {
      const result = await apiPost<ApiBuildResult>(
        "/api/c/builds",
        {
          assemblyItemId,
          qty: qty.trim(),
          builtOn,
          ...(memo.trim() ? { memo: memo.trim() } : {}),
        },
        selectedCompanyId,
      );
      setBuilt(result);
      setMemo("");
      refreshAll();
    } catch (err) {
      setBuildError(err instanceof ApiError ? err.message : "Couldn't record that build. Try again.");
    } finally {
      setBuilding(false);
    }
  }

  const canBuild = preview !== null && preview.canBuild && builtOn !== "";

  return (
    <div className="wrap">
      <header className="top">
        <h1>Builds</h1>
      </header>

      {!selectedCompanyId ? (
        <p className="msg-inline">Choose a company from the sidebar to build something.</p>
      ) : (
        <>
          <p className="sub" style={{ marginBottom: 14 }}>
            Turn parts on the shelf into a finished item. The parts come off, the finished item goes
            on, and the value moves with them.
          </p>

          {built && (
            <div className="success-banner banner-stack">
              <p>{built.summary}</p>
              <p>
                Build #{built.buildNo} &middot; {fmtQuantity(built.qty)} &times;{" "}
                {fmtUnitCost(built.unitCost)}
                {hasSubCentDigits(built.unitCost) ? ` (exactly $${exactUnitCost(built.unitCost)})` : ""}
                {" "}each &middot; {fmtMoney(built.value)} in all.
              </p>
              <div className="wizard-actions">
                <button
                  type="button"
                  className="acct-link"
                  onClick={() => navigate({ view: "item", id: built.assemblyItemId })}
                >
                  See {built.assemblyName}
                </button>
                <button
                  type="button"
                  className="acct-link"
                  onClick={() => navigate({ view: "entry", id: built.entryId })}
                >
                  See the journal entry{built.entryNo ? ` (${built.entryNo})` : ""}
                </button>
                <button type="button" className="acct-link" onClick={() => setBuilt(null)}>
                  Dismiss
                </button>
              </div>
            </div>
          )}

          <div className="card invoice-form" style={{ marginBottom: 18 }}>
            <h2>Build something</h2>

            {buildError && <ErrorBanner message={buildError} onRetry={() => setBuildError(null)} />}

            <SearchablePicker
              id="build-assembly"
              label="What are you building?"
              placeholder="Search stocked items…"
              groups={stockedGroups}
              value={assemblyItemId}
              onChange={(id) => {
                setAssemblyItemId(id);
                setBuilt(null);
              }}
              emptyMessage="No stocked items yet."
            />

            <div className="field-row">
              <div className="field">
                <label htmlFor="build-qty">How many</label>
                <input
                  id="build-qty"
                  className="text-input"
                  type="text"
                  inputMode="decimal"
                  value={qty}
                  onChange={(ev) => setQty(ev.target.value)}
                />
                {qty.trim() !== "" && !qtyOk && (
                  <div className="field-error">
                    Enter how many you&rsquo;re building — a number greater than zero, like 10 or
                    0.5.
                  </div>
                )}
              </div>
              <div className="field">
                <label htmlFor="build-date">Date</label>
                <input
                  id="build-date"
                  className="text-input"
                  type="date"
                  value={builtOn}
                  onChange={(ev) => setBuiltOn(ev.target.value)}
                />
              </div>
            </div>

            <div className="field">
              <label htmlFor="build-memo">Note (optional)</label>
              <input
                id="build-memo"
                className="text-input"
                type="text"
                placeholder="Work order 41, second run…"
                value={memo}
                onChange={(ev) => setMemo(ev.target.value)}
              />
            </div>

            {previewError && <ErrorBanner message={previewError} onRetry={() => setPreviewError(null)} />}
            {previewing && preview === null && <p className="msg-inline">Working it out&hellip;</p>}

            {preview && <BuildPreviewPanel preview={preview} navigate={navigate} />}

            {preview && !preview.canBuild && preview.blockedBecause && (
              <div className="confirm-panel" style={{ marginTop: 14 }}>
                <p>
                  <strong>Not yet.</strong> {preview.blockedBecause}
                </p>
                <p style={{ marginBottom: 0 }}>
                  Buy or receive more of what&rsquo;s short, or build fewer, and this will go
                  through. Nothing has been recorded.
                </p>
              </div>
            )}

            <div className="wizard-actions">
              <button
                type="button"
                className="btn"
                disabled={!canBuild || building}
                title={preview?.blockedBecause ?? undefined}
                onClick={() => void build()}
              >
                {building
                  ? "Building…"
                  : preview && !preview.canBuild
                    ? "Can't build this yet"
                    : "Build it"}
              </button>
            </div>

            <p className="sub" style={{ marginBottom: 0 }}>{BUILD_COST_NOTE}</p>
          </div>

          <div className="card">
            <h2>Builds so far</h2>
            {historyError && <ErrorBanner message={historyError} onRetry={retryHistory} />}
            {historyLoading && !history && <p className="msg-inline">Loading&hellip;</p>}
            {history && history.builds.length === 0 && (
              <div className="empty-state">Nothing built yet.</div>
            )}
            {history && history.builds.length > 0 && (
              <BuildHistory
                builds={history.builds}
                navigate={navigate}
                onReversed={refreshAll}
              />
            )}
          </div>
        </>
      )}
    </div>
  );
}

// ---------------------------------------------------------------------------
// The component draw
// ---------------------------------------------------------------------------

function BuildPreviewPanel({
  preview,
  navigate,
}: {
  preview: ApiBuildPreview;
  navigate: (target: NavTarget) => void;
}) {
  if (preview.components.length === 0) {
    return (
      <div className="empty-state" style={{ marginBottom: 0 }}>
        {preview.assemblyName} has no parts list yet. Open it and say what it&rsquo;s made from
        first.
        <div style={{ marginTop: 10 }}>
          <button
            type="button"
            className="acct-link"
            onClick={() => navigate({ view: "item", id: preview.assemblyItemId })}
          >
            Open {preview.assemblyName}
          </button>
        </div>
      </div>
    );
  }

  return (
    <>
      <table className="invoice-lines-table" style={{ marginTop: 14 }}>
        <thead>
          <tr>
            <th>Part</th>
            <th className="num">Needs</th>
            <th className="num">On hand</th>
            <th className="num">Short by</th>
            <th className="num">Cost each</th>
            <th className="num">Comes off the shelf</th>
          </tr>
        </thead>
        <tbody>
          {preview.components.map((c) => {
            const shortBy = c.sufficient ? null : subtractQuantity(c.qtyNeeded, c.qtyOnHand);
            return (
              <tr key={c.itemId} className={c.sufficient ? undefined : "stock-short"}>
                <td>
                  <button
                    type="button"
                    className="acct-link"
                    onClick={() => navigate({ view: "item", id: c.itemId })}
                  >
                    {c.name}
                  </button>
                  {c.sku && <div className="register-memo-sub">SKU {c.sku}</div>}
                </td>
                <td className="num">{fmtQuantity(c.qtyNeeded)}</td>
                <td className="num">{fmtQuantity(c.qtyOnHand)}</td>
                <td className="num stock-short-qty">
                  {shortBy ? fmtQuantity(shortBy) : "—"}
                </td>
                <td className="num">{fmtUnitCost(c.averageCost)}</td>
                <td className="num invoice-line-amount">{fmtMoney(c.drawValue)}</td>
              </tr>
            );
          })}
        </tbody>
      </table>

      <div className="invoice-form-total">
        <span>Parts used</span>
        <span className="invoice-form-total-amount">{fmtMoney(preview.totalValue)}</span>
      </div>
      <div className="invoice-form-total">
        <span>
          Each {preview.assemblyName} ends up worth
        </span>
        <span className="invoice-form-total-amount">{fmtUnitCost(preview.unitCost)}</span>
      </div>
      {/* Without this, ten units at an exact 37.1008 read as "$371.01 of parts"
          beside "$37.10 each" and look like they are a penny out (review F4).
          They are not — cents simply cannot hold the unit cost. Say so here,
          on the screen where the user is agreeing to the numbers. */}
      {hasSubCentDigits(preview.unitCost) && (
        <p className="line-note">
          That&rsquo;s rounded to the nearest cent. Each one works out to exactly $
          {exactUnitCost(preview.unitCost)}, which is why the parts total isn&rsquo;t
          {" "}{fmtQuantity(preview.qty)} &times; {fmtUnitCost(preview.unitCost)} exactly. The
          books use the full figure, not the rounded one.
        </p>
      )}
      {preview.inventoryAccountName && (
        <p className="sub">
          The finished {preview.assemblyName} lands in {preview.inventoryAccountName}; the parts
          come out of wherever they were sitting. The two sides are the same money, so nothing
          about your total assets changes — it has just changed shape.
        </p>
      )}
    </>
  );
}

// ---------------------------------------------------------------------------
// History + undo
// ---------------------------------------------------------------------------

function BuildHistory({
  builds,
  navigate,
  onReversed,
}: {
  builds: ApiBuildsBody["builds"];
  navigate: (target: NavTarget) => void;
  onReversed: () => void;
}) {
  const { selectedCompanyId } = useCompany();
  const [confirming, setConfirming] = useState<string | null>(null);
  const [busyId, setBusyId] = useState<string | null>(null);
  const [rowError, setRowError] = useState<{ id: string; message: string } | null>(null);
  const [undone, setUndone] = useState<{ id: string; summary: string; entryId: string } | null>(null);

  async function reverse(buildId: string) {
    if (!selectedCompanyId) return;
    setBusyId(buildId);
    setRowError(null);
    try {
      const res = await apiPost<{ buildId: string; entryId: string; summary: string }>(
        `/api/c/builds/${buildId}/reverse`, {}, selectedCompanyId,
      );
      setConfirming(null);
      setUndone({ id: buildId, summary: res.summary, entryId: res.entryId });
      onReversed();
    } catch (err) {
      setRowError({
        id: buildId,
        message: err instanceof ApiError ? err.message : "Couldn't undo that build. Try again.",
      });
    } finally {
      setBusyId(null);
    }
  }

  return (
    <>
      {undone && (
        <div className="success-banner banner-stack">
          <p>{undone.summary}</p>
          <div className="wizard-actions">
            <button
              type="button"
              className="acct-link"
              onClick={() => navigate({ view: "entry", id: undone.entryId })}
            >
              See the reversing entry
            </button>
            <button type="button" className="acct-link" onClick={() => setUndone(null)}>
              Dismiss
            </button>
          </div>
        </div>
      )}
      <table>
        <thead>
          <tr>
            <th>Build</th>
            <th>Date</th>
            <th>Item</th>
            <th className="num">Made</th>
            <th className="num">Worth</th>
            <th aria-label="Actions" />
          </tr>
        </thead>
        <tbody>
          {builds.map((b) => (
            <BuildRow
              key={b.buildId}
              build={b}
              busy={busyId === b.buildId}
              confirming={confirming === b.buildId}
              error={rowError?.id === b.buildId ? rowError.message : null}
              navigate={navigate}
              onAskUndo={() => {
                setConfirming(b.buildId);
                setRowError(null);
              }}
              onCancelUndo={() => setConfirming(null)}
              onUndo={() => void reverse(b.buildId)}
            />
          ))}
        </tbody>
      </table>
      <p className="sub" style={{ marginBottom: 0 }}>
        A build is never deleted. Undoing one records the opposite of it, so both the build and the
        undo stay in the books — which is why a year you&rsquo;ve already closed still adds up.
      </p>
    </>
  );
}

function BuildRow({
  build,
  busy,
  confirming,
  error,
  navigate,
  onAskUndo,
  onCancelUndo,
  onUndo,
}: {
  build: ApiBuildsBody["builds"][number];
  busy: boolean;
  confirming: boolean;
  error: string | null;
  navigate: (target: NavTarget) => void;
  onAskUndo: () => void;
  onCancelUndo: () => void;
  onUndo: () => void;
}) {
  const reversed = build.status === "reversed";
  return (
    <>
      <tr style={{ opacity: reversed ? 0.55 : 1 }}>
        <td>
          <button
            type="button"
            className="acct-link"
            onClick={() => navigate({ view: "entry", id: build.entryId })}
          >
            #{build.buildNo}
          </button>
          {reversed && <span className="chip chip-muted" style={{ marginLeft: 8 }}>Undone</span>}
          {build.memo && <div className="register-memo-sub">{build.memo}</div>}
        </td>
        <td style={{ whiteSpace: "nowrap" }}>{fmtDate(build.builtOn)}</td>
        <td>
          <button
            type="button"
            className="acct-link"
            onClick={() => navigate({ view: "item", id: build.assemblyItemId })}
          >
            {build.assemblyName}
          </button>
        </td>
        <td className="num">{fmtQuantity(build.qty)}</td>
        <td className="num">{fmtMoney(build.value)}</td>
        <td className="chart-actions" style={{ whiteSpace: "nowrap" }}>
          {!reversed && !confirming && (
            <button type="button" className="acct-link" disabled={busy} onClick={onAskUndo}>
              Undo
            </button>
          )}
        </td>
      </tr>
      {confirming && (
        <tr>
          <td colSpan={6}>
            <div className="confirm-panel">
              <p>
                This puts the {fmtQuantity(build.qty)} {build.assemblyName} back into parts —
                {" "}{fmtMoney(build.value)} moves back the way it came. It only works if those
                {" "}{build.assemblyName} are still on the shelf; if any have been sold or built
                into something else, we&rsquo;ll tell you instead of guessing.
              </p>
              <div className="wizard-actions">
                <button type="button" className="btn" disabled={busy} onClick={onUndo}>
                  {busy ? "Undoing…" : "Yes, undo it"}
                </button>
                <button type="button" className="acct-link" disabled={busy} onClick={onCancelUndo}>
                  Cancel
                </button>
              </div>
            </div>
          </td>
        </tr>
      )}
      {error && (
        <tr>
          <td colSpan={6} className="field-error">{error}</td>
        </tr>
      )}
    </>
  );
}
