// Exact decimal arithmetic for stock quantities and the value they carry —
// Task 5 of docs/superpowers/plans/2026-08-03-inventory.md.
//
// Everything here is BigInt integer arithmetic on the digit strings Postgres
// already handed us. Never Number(), never parseFloat, never a JS float —
// CLAUDE.md §2 forbids it for money, and a quantity that feeds a money figure
// is no different (0.1 + 0.2 is as wrong on a shelf as it is in a bank).
//
// Scales, taken from the columns themselves (0018):
//   quantity   numeric(19,4)   inventory_movements.qty, items on hand
//   unit cost  numeric(19,6)   inventory_movements.unit_cost, item_average_cost()
//   value      numeric(19,4)   inventory_movements.value
//
// Rounding is HALF-AWAY-FROM-ZERO, which is what Postgres's round() does on
// numeric and therefore what record_movement() will actually store. These
// functions exist so a SCREEN can show what a build or an adjustment is about
// to do before the round trip; the database remains the authority on every
// stored figure (CLAUDE.md §12).

const DECIMAL_RE = /^-?\d+(\.\d+)?$/;

/** Decimal string -> integer scaled by 10^places, or null if it isn't a
 *  complete decimal number yet (mid-keystroke "12." and friends). Extra
 *  fraction digits beyond `places` are truncated: every caller feeds this a
 *  value that already came out of a column of exactly that scale. */
function toScaled(value: string, places: number): bigint | null {
  const v = value.trim();
  if (!DECIMAL_RE.test(v)) return null;
  const negative = v.startsWith("-");
  const unsigned = negative ? v.slice(1) : v;
  const [wholeRaw, fracRaw = ""] = unsigned.split(".");
  const frac = (fracRaw + "0".repeat(places)).slice(0, places);
  const magnitude = BigInt((wholeRaw === "" ? "0" : wholeRaw) + frac);
  return negative ? -magnitude : magnitude;
}

/** Integer scaled by 10^places -> the NUMERIC(19,places) string shape the API
 *  itself returns, so fmtMoney/fmtQuantity/fmtUnitCost can consume it. */
function fromScaled(scaled: bigint, places: number): string {
  const negative = scaled < 0n;
  const magnitude = negative ? -scaled : scaled;
  const digits = magnitude.toString().padStart(places + 1, "0");
  const whole = digits.slice(0, digits.length - places);
  const frac = digits.slice(digits.length - places);
  const isZero = magnitude === 0n;
  return `${negative && !isZero ? "-" : ""}${whole}.${frac}`;
}

/** a + b, both NUMERIC(19,4) quantity strings. */
export function addQuantity(a: string, b: string): string | null {
  const sa = toScaled(a, 4);
  const sb = toScaled(b, 4);
  if (sa === null || sb === null) return null;
  return fromScaled(sa + sb, 4);
}

/** a − b, both NUMERIC(19,4) quantity strings. */
export function subtractQuantity(a: string, b: string): string | null {
  const sa = toScaled(a, 4);
  const sb = toScaled(b, 4);
  if (sa === null || sb === null) return null;
  return fromScaled(sa - sb, 4);
}

/** True when a quantity string is exactly zero, whatever its sign or scale. */
export function isZeroQuantity(value: string): boolean {
  const s = toScaled(value, 4);
  return s !== null && s === 0n;
}

/** Compares two quantity strings: -1, 0 or 1 (null if either isn't a number). */
export function compareQuantity(a: string, b: string): number | null {
  const sa = toScaled(a, 4);
  const sb = toScaled(b, 4);
  if (sa === null || sb === null) return null;
  return sa === sb ? 0 : sa < sb ? -1 : 1;
}

/**
 * quantity (4dp) × unit cost (6dp) -> value (4dp), rounded half-away-from-zero.
 * Mirrors record_movement()'s own `round(round(qty,4) * round(unit_cost,6), 4)`
 * exactly, so a preview and the movement that follows it agree to the digit.
 */
export function extendedCost(qty: string, unitCost: string): string | null {
  const q = toScaled(qty, 4);
  const c = toScaled(unitCost, 6);
  if (q === null || c === null) return null;
  const product = q * c; // scaled 1e10
  const divisor = 1_000_000n; // 1e10 -> 1e4
  const negative = product < 0n;
  const magnitude = negative ? -product : product;
  const rounded = (magnitude + divisor / 2n) / divisor;
  return fromScaled(negative ? -rounded : rounded, 4);
}

/**
 * True when a unit cost carries digits below a cent that rounding to cents
 * would throw away — i.e. when the "$37.10" on screen is not the whole number.
 *
 * This is what makes a build preview reconcile. A unit cost of 37.100800 shown
 * as "$37.10" beside a parts total of "$371.01" for ten units looks like an
 * arithmetic error on the one screen where the user is consenting to the
 * numbers, because 10 x $37.10 is $371.00. Nothing is wrong — the exact unit
 * cost simply has more digits than cents can hold — but the screen has to say
 * so rather than leave a manufacturer to find the penny (Task 5 review F4).
 */
export function hasSubCentDigits(unitCost: string): boolean {
  const scaled = toScaled(unitCost, 6);
  if (scaled === null) return false;
  const magnitude = scaled < 0n ? -scaled : scaled;
  return magnitude % 10_000n !== 0n;
}

/**
 * A unit cost at its full stored precision, trailing zeros trimmed:
 * "37.100800" -> "37.1008". For the footnote hasSubCentDigits() calls for —
 * never a parsed number (CLAUDE.md §2).
 */
export function exactUnitCost(unitCost: string): string {
  const v = unitCost.trim();
  if (!v.includes(".")) return v;
  return v.replace(/0+$/, "").replace(/\.$/, "") || "0";
}

/**
 * What taking `qtyOut` of an item off the shelf is worth, for a PREVIEW.
 *
 * Almost always quantity × average cost — but NOT when the item is being
 * drained to exactly zero. 0018's record_movement() assigns the whole
 * remaining value to that last movement rather than `round(qty × average, 4)`,
 * so that a fraction of a cent is never stranded in the Inventory account with
 * no quantity left to carry it out (spec as-built decision 5, which says in so
 * many words: "Reports and previews must not re-derive the last issue's cost
 * as qty × average"). This is that rule, on the screen.
 *
 * Returns a positive NUMERIC(19,4) value string, or null if the inputs aren't
 * complete numbers yet.
 */
export function issueValuePreview(
  qtyOut: string,
  qtyOnHand: string,
  averageCost: string,
  valueOnHand: string,
): string | null {
  const out = toScaled(qtyOut, 4);
  const onHand = toScaled(qtyOnHand, 4);
  if (out === null || onHand === null) return null;
  if (out === onHand) {
    const value = toScaled(valueOnHand, 4);
    return value === null ? null : fromScaled(value, 4);
  }
  return extendedCost(qtyOut, averageCost);
}
