// Shared "pick open bills to apply money to" line state — AP Task 5. Mirrors
// payments/openInvoiceLines.ts exactly (same rationale: PayBills.tsx, a brand
// new payment, and BillPaymentDetail.tsx's apply-credit mini-flow, an
// EXISTING payment's already-arrived unapplied remainder, both present the
// same shape of row — checkbox, bill reference/dates/open balance, and an
// editable "how much goes here" amount prefilled with the bill's full open
// balance) with only the AR->AP vocabulary swap (invoice -> bill, invoiceNo
// -> refNo, customer -> vendor). Pure functions, no React, all money math in
// whole-cents BigInt (CLAUDE.md §2 — never a parsed JS float, not even here
// in the browser).

/** One row of GET /api/c/vendors/:id/open-bills's `bills` array
 *  (server/src/billPayments.ts). */
export interface OpenBillApiRow {
  billId: string;
  refNo: string | null;
  billDate: 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 bill 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 OpenBillLine {
  billId: string;
  refNo: string | null;
  billDate: 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/billPayments.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-bills API response.
 *  Every row's amount box starts prefilled with that bill'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:
 *   - PayBills.tsx (new money going out): `onlyBillId` given (a bill's own
 *     "Pay this bill" button) checks only that row; otherwise every row
 *     starts checked, since the common case is paying everything owed to
 *     this vendor.
 *   - BillPaymentDetail.tsx's apply-credit mini-flow (`allUnchecked: true`,
 *     spreading an EXISTING payment's already-arrived credit across more
 *     bills): every row starts unchecked, since the available credit is
 *     usually less than the vendor's full open balance and there's no
 *     single "the" bill this credit is naturally headed for. */
export function seedLines(
  rows: OpenBillApiRow[],
  mode: { onlyBillId?: string; allUnchecked?: boolean } = {},
): OpenBillLine[] {
  return rows.map((r) => ({
    billId: r.billId,
    refNo: r.refNo,
    billDate: r.billDate,
    dueDate: r.dueDate,
    open: r.open,
    overdue: r.overdue,
    checked: mode.allUnchecked ? false : mode.onlyBillId ? r.billId === mode.onlyBillId : 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: OpenBillLine[]): 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 bill has open) as an inline hint before the round trip;
 *  pay_bills()/apply_vendor_credit()'s own RAISE remains the real guard
 *  (CLAUDE.md §12). */
export function lineAmountError(line: OpenBillLine): 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 bill's open balance.";
  }
  return null;
}

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