// #purchase-order/:id — Task 4 of docs/superpowers/plans/2026-08-03-inventory.md.
// Header, lines with their receipt progress, and the action set:
//
//   draft            Edit · Send to the vendor · Throw this draft away
//   sent / partial   Receive · Print · Cancel
//   received         Print
//   cancelled        Print
//
// RECEIVE is the whole point of the screen. It calls
// POST /api/c/purchase-orders/:id/receive, which posts NOTHING — it hands back
// a prefilled bill draft — and carries that payload into the bill form through
// a sessionStorage handoff (receiveHandoff.ts), the same pattern the bank and
// universal-import flows use. Receiving IS entering the bill: there is no
// separate receipt document and no second journal entry (spec as-built
// decision 1), which is why the receipt date is simply the bill's date.
//
// Three honesty rules this screen keeps, all of them things the API took care
// to report and that a friendlier-looking UI would quietly swallow:
//
//   1. The API's `notice` — quantities left off the prefill because they're
//      already on a bill nobody has entered — travels with the payload and is
//      shown on the bill form itself, verbatim.
//   2. The all-drafted 409 is shown as it arrives, WITH a way out: the draft
//      bill holding those quantities can be thrown away (a draft has posted
//      nothing), and this screen links straight to that vendor's bills.
//   3. Cancelling a partly-received order forecloses the rest of it
//      permanently — the status is forward-only — so the API's own summary,
//      which lists what is being given up, is displayed rather than a cheerful
//      "Cancelled".

import { useState } from "react";
import { useCompany } from "../lib/company";
import { useApiGet } from "../lib/useApiGet";
import { apiPost, apiDelete, ApiError } from "../lib/api";
import { fmtMoney } from "../lib/money";
import { fmtDate, todayIso } from "../lib/date";
import { ErrorBanner } from "../components/ErrorBanner";
import { BackLink } from "../components/BackLink";
import type { NavTarget } from "../lib/useHashView";
// fmtUnitCost, not fmtMoney, for the per-unit column: sub-cent costs are real
// (a wood screw costs $0.0042) and rounding one to cents prints "$0.00" beside
// an amount of $4.20 — a number a reader would act on, on the document they
// send a vendor. Caught in Inventory Task 5's live verification.
import { fmtQuantity, fmtUnitCost } from "../items/itemTypes";
import { setPendingVendorFilter } from "../bills/vendorFilterHandoff";
import { setPendingReceive } from "./receiveHandoff";
import { PurchaseOrderForm, type PoFormInitial } from "./PurchaseOrderForm";
import {
  poStateChip, poStateLabel, isPastExpected,
  type PoDetail, type PoLine, type ReceivePayload, type ReceiveRefusalCode,
} from "./poTypes";

export function PurchaseOrderDetail({
  poId,
  navigate,
}: {
  poId: string;
  navigate: (target: NavTarget) => void;
}) {
  const { selectedCompanyId } = useCompany();
  const { data, loading, error, retry } = useApiGet<PoDetail>(
    `/api/c/purchase-orders/${poId}`, selectedCompanyId,
  );

  const [editing, setEditing] = useState(false);
  const [busy, setBusy] = useState<"issue" | "receive" | "cancel" | "delete" | null>(null);
  const [actionError, setActionError] = useState<string | null>(null);
  /** The API's own machine-readable code for the last /receive refusal.
   *  Matched on instead of the English sentence (review finding F3): the one
   *  refusal with a way out — 'all-on-draft-bills' — used to be recognised by
   *  a regex over the prose, so a reword or a typographic apostrophe would
   *  have silently removed the only escape from that 409, which is the exact
   *  bug this screen was built to answer. */
  const [refusalCode, setRefusalCode] = useState<ReceiveRefusalCode | null>(null);
  const [cancelConfirming, setCancelConfirming] = useState(false);
  const [cancelSummary, setCancelSummary] = useState<string | null>(null);
  const [deleteConfirming, setDeleteConfirming] = useState(false);
  const [issueSummary, setIssueSummary] = useState<string | null>(null);

  async function handleIssue() {
    if (!selectedCompanyId) return;
    setBusy("issue");
    setActionError(null);
    try {
      const res = await apiPost<{ summary: string }>(
        `/api/c/purchase-orders/${poId}/issue`, undefined, selectedCompanyId,
      );
      setIssueSummary(res.summary);
      retry();
    } catch (err) {
      setActionError(err instanceof ApiError ? err.message : "Couldn't send that purchase order. Try again.");
    } finally {
      setBusy(null);
    }
  }

  async function handleReceive() {
    if (!selectedCompanyId || !data) return;
    setBusy("receive");
    setActionError(null);
    setRefusalCode(null);
    try {
      const payload = await apiPost<ReceivePayload>(
        `/api/c/purchase-orders/${poId}/receive`, undefined, selectedCompanyId,
      );
      // Nothing has been recorded by this call — the payload is a DRAFT the
      // user still reviews and enters. Hand it to the bill form.
      setPendingReceive({ ...payload, poId, poNo: data.poNo });
      navigate("bills");
    } catch (err) {
      const message = err instanceof ApiError ? err.message : "Couldn't start that receipt. Try again.";
      setActionError(message);
      const body = err instanceof ApiError ? (err.body as { code?: string } | null) : null;
      setRefusalCode((body?.code as ReceiveRefusalCode | undefined) ?? null);
    } finally {
      setBusy(null);
    }
  }

  async function handleCancel() {
    if (!selectedCompanyId) return;
    setBusy("cancel");
    setActionError(null);
    try {
      const res = await apiPost<{ summary: string; partiallyReceived: boolean }>(
        `/api/c/purchase-orders/${poId}/cancel`, undefined, selectedCompanyId,
      );
      // The API's own sentence, which names what was given up on a partly
      // received order. Shown instead of a generic success.
      setCancelSummary(res.summary);
      setCancelConfirming(false);
      retry();
    } catch (err) {
      setActionError(err instanceof ApiError ? err.message : "Couldn't cancel that purchase order. Try again.");
      setCancelConfirming(false);
    } finally {
      setBusy(null);
    }
  }

  async function handleDelete() {
    if (!selectedCompanyId) return;
    setBusy("delete");
    setActionError(null);
    try {
      await apiDelete(`/api/c/purchase-orders/${poId}`, selectedCompanyId);
      navigate("purchase-orders");
    } catch (err) {
      setActionError(err instanceof ApiError ? err.message : "Couldn't throw that draft away. Try again.");
      setDeleteConfirming(false);
    } finally {
      setBusy(null);
    }
  }

  function goToVendorsDraftBills() {
    if (!data) return;
    setPendingVendorFilter(data.vendorId);
    navigate("bills");
  }

  const initialForEdit: PoFormInitial | null = data
    ? {
        vendorId: data.vendorId,
        orderDate: data.orderDate,
        expectedDate: data.expectedDate ?? "",
        memo: data.memo ?? "",
        lines: data.lines.map((l) => ({
          itemId: l.itemId,
          description: l.description ?? "",
          quantity: fmtQuantity(l.quantity),
          unitCost: fmtQuantity(l.unitCost),
        })),
      }
    : null;

  const overdue = data ? isPastExpected(data, todayIso()) : false;
  const canReceive = data?.state === "open" || data?.state === "partial";
  // F10: Cancel is only offered where there is something left to cancel.
  // Offering it on a fully received order invited a "Cancelled. Nothing was
  // recorded in the books" reply about an order whose goods are sitting on the
  // shelf and whose bill is in A/P — true of the purchase order itself, and
  // badly misleading about the situation. The status is forward-only, so there
  // is no undo for that either.
  const canCancel = data?.state === "open" || data?.state === "partial";
  // F11: what cancelling actually gives up, named BEFORE the irreversible
  // click rather than in the summary afterwards. Only item lines can be known
  // to be outstanding (a description-only line's receipt is unobservable), so
  // only those are listed.
  const outstandingOnCancel: PoLine[] = (data?.lines ?? []).filter(
    (l) => l.itemId !== null && l.qtyOutstanding !== "0.0000",
  );
  const anythingArrived = (data?.lines ?? []).some((l) => l.qtyReceived !== "0.0000");

  return (
    <div className="wrap">
      <BackLink label="Back to purchase orders" />

      {error && <ErrorBanner message={error} onRetry={retry} />}
      {loading && !data && <p className="msg-inline">Loading&hellip;</p>}

      {data && !editing && (
        <>
          <header className="top">
            <h1>{data.poNo ? `Purchase order #${data.poNo}` : "Draft purchase order"}</h1>
            <span className={poStateChip(data.state)}>{poStateLabel(data.state)}</span>
            {overdue && <span className="chip chip-warn">Past its date</span>}
          </header>
          <div className="asof">
            {data.vendorName} · Ordered {fmtDate(data.orderDate)}
            {data.expectedDate ? ` · Expected by ${fmtDate(data.expectedDate)}` : ""}
          </div>

          <p className="badge" style={{ marginTop: 10, display: "inline-flex" }}>
            No accounting impact — a purchase order never touches the books
          </p>

          {issueSummary && <p className="msg-inline po-receive-notice">{issueSummary}</p>}
          {cancelSummary && <p className="msg-inline po-receive-notice">{cancelSummary}</p>}

          {data.overReceivedLines > 0 && (
            <p className="msg-inline po-receive-notice">
              More arrived than was ordered on {data.overReceivedLines} line
              {data.overReceivedLines === 1 ? "" : "s"}. That&rsquo;s allowed — the bill records
              what the vendor actually sent — but it&rsquo;s worth a look at the paperwork.
            </p>
          )}
          {data.unreceivableLines > 0 && (
            <p className="msg-inline po-receive-notice">
              {data.unreceivableLines} line{data.unreceivableLines === 1 ? " has" : "s have"} no
              item on {data.unreceivableLines === 1 ? "it" : "them"}, so nothing can mark
              {data.unreceivableLines === 1 ? " it" : " them"} received. Any quantity still shown
              outstanding on those lines means &ldquo;we can&rsquo;t tell&rdquo;, not
              &ldquo;it hasn&rsquo;t arrived&rdquo;.
            </p>
          )}

          <div className="card" style={{ margin: "18px 0" }}>
            <h2>Lines</h2>
            <table>
              <thead>
                <tr>
                  <th>Item</th>
                  <th>Description</th>
                  <th className="num">Ordered</th>
                  <th className="num">Received</th>
                  <th className="num">Still outstanding</th>
                  <th className="num">Unit cost</th>
                  <th className="num">Amount</th>
                </tr>
              </thead>
              <tbody>
                {data.lines.map((l) => (
                  <tr key={l.id}>
                    <td>
                      {l.itemId ? (
                        <button
                          type="button"
                          className="acct-link"
                          onClick={() => navigate({ view: "item", id: l.itemId! })}
                        >
                          {l.itemName}
                        </button>
                      ) : (
                        <span className="sub">Not an item</span>
                      )}
                    </td>
                    <td>{l.description ?? "—"}</td>
                    <td className="num">{fmtQuantity(l.quantity)}</td>
                    <td className="num">
                      {l.itemId ? fmtQuantity(l.qtyReceived) : "—"}
                      {l.overReceived && (
                        <div className="register-memo-sub">more than ordered</div>
                      )}
                      {l.qtyOnDraftBills !== "0.0000" && (
                        <div className="register-memo-sub">
                          {fmtQuantity(l.qtyOnDraftBills)} on a bill not entered yet
                        </div>
                      )}
                    </td>
                    <td className="num">{l.itemId ? fmtQuantity(l.qtyOutstanding) : "—"}</td>
                    <td className="num">{fmtUnitCost(l.unitCost)}</td>
                    <td className="num">{fmtMoney(l.amount)}</td>
                  </tr>
                ))}
              </tbody>
            </table>

            <div className="rows" style={{ marginTop: 12 }}>
              <div className="row total">
                <span className="n">Order total</span>
                <span className="a">{fmtMoney(data.total)}</span>
              </div>
            </div>
          </div>

          {data.memo && (
            <div className="card" style={{ marginBottom: 18 }}>
              <h2>Memo</h2>
              <p>{data.memo}</p>
            </div>
          )}

          <div className="card">
            <h2>Actions</h2>
            {actionError && (
              <>
                <ErrorBanner message={actionError} onRetry={() => setActionError(null)} />
                {refusalCode === "all-on-draft-bills" && (
                  <p className="sub">
                    That bill hasn&rsquo;t been entered, so nothing has been recorded and no stock
                    has moved. Finish it, or throw it away from its own page — either way this
                    order picks up where it left off.{" "}
                    <button type="button" className="acct-link" onClick={goToVendorsDraftBills}>
                      See {data.vendorName}&rsquo;s bills
                    </button>
                  </p>
                )}
              </>
            )}

            {deleteConfirming ? (
              <div className="confirm-panel">
                <p>
                  This throws the draft away for good. Nothing was ever recorded — a purchase order
                  doesn&rsquo;t touch your books, and this one was never even sent.
                </p>
                <div className="wizard-actions">
                  <button type="button" className="btn" disabled={busy !== null} onClick={() => void handleDelete()}>
                    {busy === "delete" ? "Throwing away…" : "Yes, throw it away"}
                  </button>
                  <button
                    type="button"
                    className="acct-link"
                    disabled={busy !== null}
                    onClick={() => setDeleteConfirming(false)}
                  >
                    Cancel
                  </button>
                </div>
              </div>
            ) : cancelConfirming ? (
              <div className="confirm-panel">
                <p>
                  Cancelling closes this order off for good — nothing more can be received against
                  it.{" "}
                  {anythingArrived
                    ? "Some of it has already arrived; what has arrived stays in your books exactly as it is, and the rest is given up."
                    : "Nothing has arrived against it, and nothing is recorded in your books either way."}{" "}
                  If you still need the goods, raise a new order.
                </p>
                {/* F11: the quantities being given up, named before the click
                    rather than reported after it. */}
                {outstandingOnCancel.length > 0 && (
                  <>
                    <p>You&rsquo;d be giving up:</p>
                    <ul className="entry-lines">
                      {outstandingOnCancel.map((l) => (
                        <li key={l.id}>
                          {fmtQuantity(l.qtyOutstanding)} {l.itemName ?? l.description ?? "items"}
                          {l.qtyOnDraftBills !== "0.0000" && (
                            <> — including {fmtQuantity(l.qtyOnDraftBills)} already on a bill you
                              haven&rsquo;t entered</>
                          )}
                        </li>
                      ))}
                    </ul>
                  </>
                )}
                <div className="wizard-actions">
                  <button type="button" className="btn" disabled={busy !== null} onClick={() => void handleCancel()}>
                    {busy === "cancel" ? "Cancelling…" : "Yes, cancel it"}
                  </button>
                  <button
                    type="button"
                    className="acct-link"
                    disabled={busy !== null}
                    onClick={() => setCancelConfirming(false)}
                  >
                    Keep it open
                  </button>
                </div>
              </div>
            ) : (
              <div className="wizard-actions">
                {data.state === "draft" && (
                  <>
                    <button type="button" className="btn" onClick={() => setEditing(true)}>
                      Edit
                    </button>
                    <button
                      type="button"
                      className="btn"
                      disabled={busy !== null}
                      onClick={() => void handleIssue()}
                    >
                      {busy === "issue" ? "Sending…" : "Send to the vendor"}
                    </button>
                    <button type="button" className="acct-link" onClick={() => setDeleteConfirming(true)}>
                      Throw this draft away
                    </button>
                  </>
                )}
                {canReceive && (
                  <button
                    type="button"
                    className="btn"
                    disabled={busy !== null}
                    onClick={() => void handleReceive()}
                  >
                    {busy === "receive" ? "Starting…" : "Receive"}
                  </button>
                )}
                {data.poNo && (
                  <button
                    type="button"
                    className="btn"
                    onClick={() => navigate({ view: "purchase-order-print", id: poId })}
                  >
                    Print
                  </button>
                )}
                {canCancel && (
                  <button type="button" className="acct-link" onClick={() => setCancelConfirming(true)}>
                    Cancel this order
                  </button>
                )}
              </div>
            )}

            {canReceive && !cancelConfirming && !deleteConfirming && (
              <p className="sub" style={{ marginBottom: 0 }}>
                &ldquo;Receive&rdquo; starts the vendor&rsquo;s bill with what&rsquo;s still
                outstanding already on it. Entering that bill is what records the goods and the
                money — together, on the same date.
              </p>
            )}
          </div>
        </>
      )}

      {data && editing && initialForEdit && (
        <PurchaseOrderForm
          mode="edit"
          poId={poId}
          initial={initialForEdit}
          onCancel={() => setEditing(false)}
          onSaved={() => {
            setEditing(false);
            retry();
          }}
        />
      )}
    </div>
  );
}
