// Money formatting for the UI. Amounts arrive from the API as NUMERIC(19,4)
// strings ("1234.5600", "-1234.5600", "0.0000") — CLAUDE.md §2 forbids ever
// putting a money value through parseFloat/Number()/JS arithmetic, even in
// the browser, even just to format it. Every function here does its
// rounding and comma-insertion with plain string/BigInt integer math, never
// float math, and never touches magnitude via Number() for a money value.
//
// The one narrow, explicitly-blessed exception (CLAUDE.md §2 is about
// *values*, not axis geometry) lives in components/BarsChart.tsx, where
// Number() is used only to size SVG bars/gridlines — see the comment there.

const MONEY_RE = /^(-)?(\d+)\.(\d{4})$/;

/**
 * Formats a debit-positive-storage NUMERIC(19,4) string as
 * "$1,234.56" / "-$1,234.56", rounding 4 decimals down to 2 (cents) with
 * round-half-up, using BigInt so no float ever enters the calculation.
 * A value that rounds to exactly zero never shows a leading "-" (mirrors
 * the server's flipSign() zero-normalization in server/src/reports.ts).
 */
export function fmtMoney(amount: string): string {
  const match = MONEY_RE.exec(amount.trim());
  if (!match) return amount; // defensive: unexpected shape, surface raw rather than throw
  const [, sign, intPart, fracPart] = match as unknown as [string, string | undefined, string, string];

  // scaled = the value * 10000, exact (string concatenation of the two
  // integer parts Postgres already gave us — no parsing of a decimal point).
  const scaled = BigInt(intPart + fracPart);
  // Round-half-up from *10000 to *100 (cents): integer division after adding
  // half the divisor. BigInt division truncates toward zero, which is floor
  // here since scaled+50 is always >= 0.
  const cents = (scaled + 50n) / 100n;
  const isZero = cents === 0n;

  const centsStr = cents.toString().padStart(3, "0");
  const whole = centsStr.slice(0, -2);
  const centPart = centsStr.slice(-2);
  const withCommas = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");

  return `${!isZero && sign ? "-" : ""}$${withCommas}.${centPart}`;
}

/**
 * Exact decimal subtraction of two NUMERIC(19,4) strings, done with BigInt
 * (never Number()/parseFloat), returning a NUMERIC(19,4)-shaped string.
 * Used only for presentation-time derived figures that the API doesn't
 * already hand over as a field (e.g. a per-year "kept" column built from
 * years[].moneyIn/moneyOut) — nothing here is stored or cached, so this
 * isn't the "stored computed balance" CLAUDE.md §12 warns against.
 */
export function subtractMoney(a: string, b: string): string {
  const ma = MONEY_RE.exec(a.trim());
  const mb = MONEY_RE.exec(b.trim());
  if (!ma || !mb) return "0.0000";
  const va = (ma[1] ? -1n : 1n) * BigInt(ma[2] + ma[3]);
  const vb = (mb[1] ? -1n : 1n) * BigInt(mb[2] + mb[3]);
  const diff = va - vb;
  const negative = diff < 0n;
  const digits = (negative ? -diff : diff).toString().padStart(5, "0");
  const whole = digits.slice(0, -4);
  const frac = digits.slice(-4);
  const isZero = whole === "0".repeat(whole.length) && frac === "0000";
  return `${negative && !isZero ? "-" : ""}${whole}.${frac}`;
}

/** True when a NUMERIC(19,4) string is exactly zero (any valid sign/format). */
export function isZeroMoney(amount: string): boolean {
  return /^-?0+(\.0+)?$/.test(amount.trim());
}

/** Strips a leading "-" from a NUMERIC(19,4) string — pure string surgery,
 * for the rare presentation cases (e.g. the out-of-balance chip) that only
 * ever care about magnitude, never sign. */
export function absMoney(amount: string): string {
  return amount.trim().startsWith("-") ? amount.trim().slice(1) : amount.trim();
}

/**
 * Formats a user-TYPED amount string (already known to match the entries API's
 * amountSchema, /^(0|[1-9]\d*)(\.\d{1,2})?$/ — at most 2 decimal places, no
 * leading zeros, no sign) as "$1,234.50" — mirrors server/src/entries.ts's
 * formatUsd() exactly (same split-on-'.', pad-to-2, comma-group approach) so
 * the Record screen's live preview sentence (Task 11) reads identically to
 * the summary the server itself returns after posting. Deliberately separate
 * from fmtMoney() above: that function's input is always a NUMERIC(19,4)
 * string *from the database* (4 fraction digits, optional leading '-'); this
 * one's input is raw user keystrokes (0-2 fraction digits, never negative) —
 * different shapes, so no float ever needs to enter either path (CLAUDE.md §2).
 */
export function formatEnteredAmount(amount: string): string {
  const [wholeRaw, fracRaw = ""] = amount.trim().split(".");
  const whole = (wholeRaw || "0").replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  const frac = (fracRaw + "00").slice(0, 2);
  return `$${whole}.${frac}`;
}

/**
 * Formats an already-signed "amount into this account" value — exactly what
 * GET /api/c/accounts/:id/entries returns as `amountIntoAccount` (positive =
 * moved INTO the account per its own normal side; negative = moved OUT) — as
 * the account drill-through's running English: "+$500.00 in" / "-$120.00
 * out". Sign detection is the same leading-"-" string check absMoney/
 * isZeroMoney use; magnitude formatting is fmtMoney on the stripped string,
 * so this never introduces a second BigInt code path.
 */
export function fmtSignedInto(amount: string): string {
  const negative = amount.trim().startsWith("-") && !isZeroMoney(amount);
  const magnitude = fmtMoney(absMoney(amount));
  return negative ? `-${magnitude} out` : `+${magnitude} in`;
}
