// Shared purchase-order shapes and vocabulary — Task 4 of
// docs/superpowers/plans/2026-08-03-inventory.md.
//
// `status` (stored) and `state` (derived) are BOTH on the wire and they are
// not the same thing: server/src/purchaseOrders.ts stores only
// draft/open/cancelled and derives partial/closed at query time by summing
// what has actually been received, because CLAUDE.md §3.4 says nothing
// computable is cached. Every screen shows `state`; `status` is only consulted
// where the question is genuinely about the stored lifecycle (may this still
// be edited?).

export type PoState = "draft" | "open" | "partial" | "closed" | "cancelled";

const PO_STATE_LABEL: Record<PoState, string> = {
  draft: "Draft",
  open: "Sent",
  partial: "Partly received",
  closed: "Received",
  cancelled: "Cancelled",
};

const PO_STATE_CHIP: Record<PoState, string> = {
  draft: "chip chip-muted",
  open: "chip chip-info",
  partial: "chip chip-warn",
  closed: "chip",
  cancelled: "chip chip-muted",
};

/** The state's label, defensive against a state this build doesn't know —
 *  0018's CHECK permits five values and a future explicit "close this early"
 *  would be a sixth. An unrecognised state shows its own name rather than
 *  rendering `undefined` into the page (same pattern as itemTypes.ts's
 *  kindLabel; review finding F9). */
export function poStateLabel(state: string): string {
  return PO_STATE_LABEL[state as PoState] ?? state;
}

/** The state's chip class, falling back to the neutral chip. */
export function poStateChip(state: string): string {
  return PO_STATE_CHIP[state as PoState] ?? "chip chip-muted";
}

/** Refusal codes from POST /api/c/purchase-orders/:id/receive
 *  (server/src/purchaseOrders.ts). Matched on instead of the English prose,
 *  which is free to be reworded — review finding F3. */
export type ReceiveRefusalCode =
  | "not-sent" | "cancelled" | "all-on-draft-bills" | "fully-received";

export interface PoLine {
  id: string;
  itemId: string | null;
  itemName: string | null;
  itemKind: string | null;
  description: string | null;
  quantity: string;
  unitCost: string;
  amount: string;
  qtyReceived: string;
  /** Sitting on a bill somebody started but hasn't entered — it has posted
   *  nothing, so it never counts as received, but it IS why a new receipt
   *  asks for less than the outstanding quantity. */
  qtyOnDraftBills: string;
  qtyOutstanding: string;
  qtyAvailableToReceive: string;
  overReceived: boolean;
}

export interface PoDetail {
  id: string;
  poNo: string | null;
  vendorId: string;
  vendorName: string;
  orderDate: string;
  expectedDate: string | null;
  memo: string | null;
  status: string;
  state: PoState;
  total: string;
  receivableLines: number;
  /** Description-only lines: nothing can ever mark one received, so a mixed
   *  order can read "Received" while one of these still shows an outstanding
   *  quantity. Said out loud rather than left to look like a contradiction. */
  unreceivableLines: number;
  overReceivedLines: number;
  lines: PoLine[];
}

export interface PoListRow {
  id: string;
  poNo: string | null;
  vendorId: string;
  vendorName: string;
  orderDate: string;
  expectedDate: string | null;
  memo: string | null;
  status: string;
  state: PoState;
  total: string;
  receivableLines: number;
  unreceivableLines: number;
  overReceivedLines: number;
  filledLines: number;
}

export interface PoListResponse {
  total: number;
  purchaseOrders: PoListRow[];
}

/** One line of POST /api/c/purchase-orders/:id/receive's prefilled bill. */
export interface ReceiveLine {
  poLineId: string | null;
  itemId: string | null;
  itemName: string | null;
  description: string | null;
  quantity: string;
  qtyOnDraftBills: string;
  unitPrice: string;
  /** A description-only purchase order line can't carry a po_line_id on a
   *  bill (bill_lines_po_line_requires_item), so it arrives as an ordinary
   *  line the user still has to categorise. */
  needsCategory: boolean;
}

export interface ReceivePayload {
  vendorId: string;
  vendorName: string;
  billDate: string;
  memo: string | null;
  /** Non-null when quantities were subtracted because they're already on a
   *  bill nobody has entered yet. Shown verbatim. */
  notice: string | null;
  lines: ReceiveLine[];
}

/** True when an issued, still-open order is past the date it was expected.
 *  Compared as ISO strings, which sort chronologically — no Date parsing, so
 *  no timezone can roll the day backwards (lib/date.ts's standing warning). */
export function isPastExpected(row: { state: PoState; expectedDate: string | null }, todayIso: string): boolean {
  if (row.expectedDate === null) return false;
  if (row.state !== "open" && row.state !== "partial") return false;
  return row.expectedDate < todayIso;
}
