// Shared item shapes + the plain-English vocabulary every item screen uses —
// Task 4 of docs/superpowers/plans/2026-08-03-inventory.md.
//
// The stored values stay inventory/non_inventory/service (server/src/items.ts's
// ITEM_KINDS); those tokens never reach a user. CLAUDE.md §9 as amended
// 2026-08-02: standard accounting terminology is legitimate and visible, with
// the plain-English sentence layered ON TOP — so the headings say "Stocked
// items" and the picker says "Stocked item — you keep it on hand", never
// "inventory" as a bare enum and never a euphemism that hides what it is.

import { fmtMoney } from "../lib/money.js";

export const ITEM_KINDS = ["inventory", "non_inventory", "service"] as const;
export type ItemKind = (typeof ITEM_KINDS)[number];

/** Section headings on the Items screen, in the order they're shown. */
export const KIND_HEADINGS: Record<ItemKind, string> = {
  inventory: "Stocked items",
  non_inventory: "Non-stocked items",
  service: "Services",
};

/** Singular label — chips, detail headers, picker group labels. */
export const KIND_LABELS: Record<ItemKind, string> = {
  inventory: "Stocked item",
  non_inventory: "Non-stocked item",
  service: "Service",
};

/** The one-line explanation shown beside each choice in the item modal. */
export const KIND_NOTES: Record<ItemKind, string> = {
  inventory: "you keep it on hand, and we track the quantity and what it's worth",
  non_inventory: "you buy and sell it, but you don't track a quantity",
  service: "labor or time, with nothing to keep on a shelf",
};

/** What each kind explains about itself on the Items screen. */
export const KIND_SECTION_NOTES: Record<ItemKind, string> = {
  inventory:
    "Counted and valued at average cost. Buying one adds to stock; selling one takes it away and records what it cost you.",
  non_inventory:
    "Bought and sold without a quantity on hand — the cost goes straight to the account below.",
  service: "Nothing to stock, so no quantity and no cost of goods.",
};

/** One row of GET /api/c/items (server/src/items.ts's toItemBody). */
export interface ApiItem {
  id: string;
  sku: string | null;
  name: string;
  kind: string;
  incomeAccountId: string | null;
  incomeAccountName: string | null;
  cogsAccountId: string | null;
  cogsAccountName: string | null;
  inventoryAccountId: string | null;
  inventoryAccountName: string | null;
  salesPrice: string | null;
  purchaseCost: string | null;
  description: string | null;
  active: boolean;
  /** Only a stocked item has these — null for everything else, never 0 (a
   *  service reading "0 on hand" would read as "we're out of it"). */
  qtyOnHand: string | null;
  averageCost: string | null;
  value: string | null;
}

/** One row of GET /api/c/items/:id's `movements` — the item's stock history,
 *  newest first, each carrying the running quantity as of that movement
 *  (computed over the item's WHOLE history in date order, server-side). */
export interface ApiMovement {
  id: string;
  movedOn: string;
  /** Signed: positive in, negative out. */
  qty: string;
  unitCost: string;
  value: string;
  reason: string;
  sourceKind: string | null;
  sourceId: string | null;
  entryId: string;
  entryNo: string | null;
  entryMemo: string | null;
  runningQty: string;
}

export interface ApiItemDetail extends ApiItem {
  movements: ApiMovement[];
}

export function isItemKind(value: string): value is ItemKind {
  return (ITEM_KINDS as readonly string[]).includes(value);
}

/** The kind's heading, defensive against a kind this build doesn't know. */
export function kindLabel(kind: string): string {
  return isItemKind(kind) ? KIND_LABELS[kind] : kind;
}

/** "1.0000" -> "1", "0.5000" -> "0.5", "-3.0000" -> "-3". A quantity is not a
 *  monetary value (CLAUDE.md §2 is about money), so plain string trimming is
 *  fine — mirrors server/src/purchaseOrders.ts's formatQuantity() and
 *  BillDetail.tsx's fmtQuantity(). */
export function fmtQuantity(q: string): string {
  if (!q.includes(".")) return q;
  return q.replace(/0+$/, "").replace(/\.$/, "");
}

/** Magnitude of a signed quantity string, for the In/Out columns. */
export function absQuantity(q: string): string {
  return q.trim().startsWith("-") ? q.trim().slice(1) : q.trim();
}

/**
 * A stored numeric(19,4) back to what a person would type into the field it
 * came from: "12.5000" -> "12.5", "0.0042" -> "0.0042", "1.0000" -> "1".
 *
 * The `includes(".")` guard is the whole point and is easy to leave out — the
 * trailing-zero strip is unconditional otherwise, so a whole number that
 * reached this without its decimal part ("100") would come back "1". That is
 * latent for values straight off the API (always four decimal places) and very
 * much not latent for anything a form has already trimmed once. Shared rather
 * than re-derived per screen for exactly that reason (Inventory Task 5 review
 * F6). Pure string surgery, never a parsed number (CLAUDE.md §2).
 */
export function trimStoredDecimal(value: string): string {
  const v = value.trim();
  if (!v.includes(".")) return v;
  return v.replace(/0+$/, "").replace(/\.$/, "") || "0";
}

/**
 * A PER-UNIT cost, which is not the same shape as money and must not be run
 * through fmtMoney() alone.
 *
 * 0018 gave inventory_movements.unit_cost — and therefore item_average_cost()
 * and inventory_valuation() — SIX decimal places, because sub-cent costs are
 * real: a washer costs $0.0042. fmtMoney()'s regex expects exactly the four
 * of a numeric(19,4) and returns its input untouched otherwise, so a
 * six-decimal average reaches the screen as the raw string "0.004200" (caught
 * in this task's live verification, where a brand-new item's average cost
 * rendered as "0.000000" instead of "$0.00").
 *
 * Rounding it to cents unconditionally is the other wrong answer: it prints
 * "$0.00" for a washer, which is a number the reader would act on. So:
 *   - a cost that rounds to at least a cent is ordinary money, shown as money;
 *   - a non-zero cost BELOW half a cent keeps the digits that make it non-zero
 *     ($0.0042) — the honest rendering, and the reason the column exists;
 *   - exactly zero is $0.00.
 *
 * All string/BigInt integer arithmetic, never a parsed float (CLAUDE.md §2).
 */
export function fmtUnitCost(raw: string): string {
  const match = /^(-?)(\d+)(?:\.(\d+))?$/.exec(raw.trim());
  if (!match) return raw;
  const sign = match[1] ?? "";
  const whole = match[2]!;
  const frac = (match[3] ?? "").padEnd(6, "0").slice(0, 6);
  const millionths = BigInt(whole + frac);
  if (millionths === 0n) return fmtMoney("0.0000");
  // Rounds to at least one cent? Then it's ordinary money.
  const cents = (millionths + 5000n) / 10000n;
  if (cents !== 0n) return fmtMoney(`${sign}${whole}.${frac.slice(0, 4)}`);
  return `${sign}$${whole}.${frac.replace(/0+$/, "")}`;
}

/**
 * What a movement DID, in plain English.
 *
 * The reason alone isn't enough: db/migrations/0021_inventory_account_adoption.sql's
 * reverse_movement() writes its compensating row with the SAME reason and a
 * flipped sign, so a voided receipt is a `receipt` with a negative quantity.
 * Labelling that "Received" while showing -10 would be a small lie in the one
 * place a user goes to find out what happened, so the sign is read too.
 */
export function movementLabel(reason: string, qty: string): string {
  const out = qty.trim().startsWith("-");
  switch (reason) {
    case "receipt": return out ? "Receipt undone" : "Received on a bill";
    case "sale": return out ? "Sold on an invoice" : "Sale undone";
    case "build_in": return out ? "Build undone" : "Built";
    case "build_out": return out ? "Used in a build" : "Build undone — parts put back";
    case "adjustment": return out ? "Adjustment out" : "Adjustment in";
    case "opening": return out ? "Opening count corrected" : "Opening count";
    default: return out ? "Stock out" : "Stock in";
  }
}
