// Shared "pick open invoices to apply money to" line state — AR Task 6. Both
// ReceivePayment.tsx (a brand-new payment: money + applications together)
// and PaymentDetail.tsx's apply-credit mini-flow (an EXISTING payment's
// already-arrived unapplied remainder, spread across more invoices, no new
// money) present the same shape of row: a checkbox, the invoice's number/
// date/due date/open balance, and an editable "how much of this payment goes
// here" amount that starts prefilled with the invoice's full open balance.
// Factoring the seeding/parsing/summing logic out here (as pure functions,
// no React) keeps both call sites from re-deriving the same BigInt-only
// money math (CLAUDE.md §2 — never a parsed JS float, not even here in the
// browser) two different ways.

/** One row of GET /api/c/customers/:id/open-invoices's `invoices` array
 *  (server/src/payments.ts). */
export interface OpenInvoiceApiRow {
  invoiceId: string;
  invoiceNo: string | null;
  invoiceDate: string;
  dueDate: string;
  total: string;
  open: string; // NUMERIC(19,4) — the ceiling for this line's amount
  overdue: boolean;
}

/** One editable row in the picker: a real invoice plus the two bits of UI
 *  state (checked, and the raw dollars-and-cents string the user is typing
 *  into its amount box). */
export interface OpenInvoiceLine {
  invoiceId: string;
  invoiceNo: string | null;
  invoiceDate: string;
  dueDate: string;
  open: string; // NUMERIC(19,4), unchanged from the API row
  overdue: boolean;
  checked: boolean;
  amount: string; // raw entered text, e.g. "120.5" — never parsed as a float
}

// Dollars-and-cents entered amount: same shape as Record.tsx's AMOUNT_RE /
// server/src/payments.ts's amountSchema — up to 2 decimal places, no leading
// zeros, no sign (every line here is money being applied, never negative).
const ENTERED_AMOUNT_RE = /^(0|[1-9]\d{0,12})(\.\d{1,2})?$/;

/** Parses a NUMERIC(19,4) string from the API ("120.5000") into whole cents
 *  as a BigInt, rounding half-up exactly like web/src/lib/money.ts's
 *  fmtMoney() — so a line's cap and its displayed value never disagree by a
 *  rounding hair. Returns 0n for anything that doesn't match the expected
 *  shape (defensive only; every caller here is quoting a value the server
 *  itself just returned). */
export function centsOfNumeric(numeric: string): bigint {
  const match = /^(-)?(\d+)\.(\d{4})$/.exec(numeric.trim());
  if (!match) return 0n;
  const [, sign, intPart, fracPart] = match as unknown as [string, string | undefined, string, string];
  const scaled = BigInt(intPart + fracPart);
  const cents = (scaled + 50n) / 100n;
  return sign ? -cents : cents;
}

/** Parses a raw user-entered dollars string ("120.5", "120", "") into whole
 *  cents as a BigInt. Blank or malformed input is treated as 0 — this is
 *  used for running totals that need to update on every keystroke, including
 *  the ones mid-edit that aren't a complete number yet; `lineAmountError`
 *  below is what actually surfaces a validation message for those. */
export function centsOfEntered(entered: string): bigint {
  const raw = entered.trim();
  if (!ENTERED_AMOUNT_RE.test(raw)) return 0n;
  const [wholeRaw, fracRaw = ""] = raw.split(".");
  const frac = (fracRaw + "00").slice(0, 2);
  return BigInt(wholeRaw || "0") * 100n + BigInt(frac);
}

/** Renders whole cents back to a plain (no "$", no commas) dollars string
 *  suitable for an <input> value, e.g. 12050n -> "120.50". */
export function centsToEntered(cents: bigint): string {
  const negative = cents < 0n;
  const magnitude = negative ? -cents : cents;
  const digits = magnitude.toString().padStart(3, "0");
  const whole = digits.slice(0, -2);
  const frac = digits.slice(-2);
  return `${negative ? "-" : ""}${whole}.${frac}`;
}

/** "120.5000" -> "120.50" — the API's open balance, reshaped into the same
 *  plain entered-amount form used to prefill a line's amount box. */
export function numericToEntered(numeric: string): string {
  return centsToEntered(centsOfNumeric(numeric));
}

/** Builds the picker's initial row set from the open-invoices API response.
 *  Every row's amount box starts prefilled with that invoice's full open
 *  balance regardless of mode — unchecking a row just excludes it from the
 *  running total, it doesn't clear the box, so re-checking it doesn't lose
 *  whatever amount was there. Two modes, matching the two callers:
 *   - ReceivePayment.tsx (new money arriving): `onlyInvoiceId` given (the
 *     "Receive payment" button on one specific invoice's own detail page)
 *     checks only that row; otherwise every row starts checked, since the
 *     common case is a customer paying everything they owe.
 *   - PaymentDetail.tsx's apply-credit mini-flow (`allUnchecked: true`,
 *     spreading an EXISTING payment's already-arrived credit across more
 *     invoices): every row starts unchecked, since the available credit is
 *     usually less than the customer's full open balance and there's no
 *     single "the" invoice this credit is naturally headed for. */
export function seedLines(
  rows: OpenInvoiceApiRow[],
  mode: { onlyInvoiceId?: string; allUnchecked?: boolean } = {},
): OpenInvoiceLine[] {
  return rows.map((r) => ({
    invoiceId: r.invoiceId,
    invoiceNo: r.invoiceNo,
    invoiceDate: r.invoiceDate,
    dueDate: r.dueDate,
    open: r.open,
    overdue: r.overdue,
    checked: mode.allUnchecked ? false : mode.onlyInvoiceId ? r.invoiceId === mode.onlyInvoiceId : true,
    amount: numericToEntered(r.open),
  }));
}

/** Sum, in whole cents, of every CHECKED line's entered amount. Unchecked
 *  lines never contribute, regardless of what's sitting in their box. */
export function sumCheckedCents(lines: OpenInvoiceLine[]): bigint {
  return lines.filter((l) => l.checked).reduce((sum, l) => sum + centsOfEntered(l.amount), 0n);
}

/** English validation for one line's amount box — checked lines only (an
 *  unchecked line's amount is never submitted, so it's never wrong). Mirrors
 *  the two things the server itself would refuse (blank/malformed, or more
 *  than the invoice has open) as an inline hint before the round trip;
 *  receive_payment()/apply_credit()'s own RAISE remains the real guard
 *  (CLAUDE.md §12). */
export function lineAmountError(line: OpenInvoiceLine): string | null {
  if (!line.checked) return null;
  const raw = line.amount.trim();
  if (raw === "") return "Enter an amount to apply.";
  if (!ENTERED_AMOUNT_RE.test(raw) || !/[1-9]/.test(raw)) {
    return "Enter an amount like 120 or 120.50.";
  }
  if (centsOfEntered(raw) > centsOfNumeric(line.open)) {
    return "That's more than this invoice's open balance.";
  }
  return null;
}

/** Builds the `applications` array POST /api/c/payments and POST
 *  /api/c/payments/:id/apply both expect: one {invoiceId, amount} per
 *  checked, non-blank line. */
export function buildApplications(lines: OpenInvoiceLine[]): Array<{ invoiceId: string; amount: string }> {
  return lines
    .filter((l) => l.checked && l.amount.trim() !== "")
    .map((l) => ({ invoiceId: l.invoiceId, amount: l.amount.trim() }));
}
