'use strict';

/**
 * Money utilities — integer-cents core.
 *
 * All arithmetic runs in integer cents so sums and rounding are exact;
 * floats only appear at the edges (DB columns that still store dollar
 * REALs, API payloads, display formatting).
 *
 * Storage units today:
 *   - bills / payments / monthly_bill_state / budgets / income: dollars (REAL)
 *   - SimpleFIN transactions / financial_accounts:              cents (INTEGER)
 *
 * The planned v1.03 migration (see docs/cents-migration-plan.md) converts the
 * dollar columns to integer cents. Until then, services work in dollars at
 * their boundaries and these helpers guarantee cent-exact math internally.
 */

/**
 * Dollars (number or string like "$1,234.56") → integer cents.
 * null/undefined/'' → null. Unparseable input → NaN (caller validates).
 *
 * Rounds off the decimal string (via the shortest round-trip representation for
 * numbers) rather than `Math.round(n * 100)`, whose binary-float error rounds
 * e.g. 1.005 down to 100 instead of 101 (QA-B7-01). Output is identical to the
 * old helper for all integer / ≤2-decimal / "$1,234.56" inputs; only 3+-decimal
 * half-cent values change, and they now round half away from zero.
 */
function toCents(dollars) {
  if (dollars === null || dollars === undefined || dollars === '') return null;
  const cleaned = typeof dollars === 'string' ? dollars.replace(/[$,\s]/g, '') : dollars;
  const n = Number(cleaned);
  if (!Number.isFinite(n)) return NaN;

  // The shortest decimal string that round-trips to this float (e.g. 1.005 for
  // the number 1.005, not 1.00499999…). Scientific notation → float fallback.
  const decimal = typeof cleaned === 'string' ? cleaned : n.toString();
  if (/[eE]/.test(decimal)) return Math.round(n * 100);

  const negative = decimal.trim().startsWith('-');
  const [intPart, fracPart = ''] = decimal.replace('-', '').split('.');
  const frac3 = (fracPart + '000').slice(0, 3);
  const cents = Number(intPart || '0') * 100 + Number(frac3.slice(0, 2)) + (Number(frac3[2]) >= 5 ? 1 : 0);
  return negative ? -cents : cents;
}

/** Integer cents → dollar number (for API payloads). null/undefined → null. */
function fromCents(cents) {
  if (cents === null || cents === undefined) return null;
  const n = Number(cents);
  if (!Number.isFinite(n)) return null;
  return n / 100;
}

/**
 * Round a dollar amount to the nearest cent, exactly.
 * Drop-in replacement for the old `Math.round(x * 100) / 100` helpers.
 */
function roundMoney(value) {
  const cents = toCents(value);
  return cents === null || Number.isNaN(cents) ? 0 : cents / 100;
}

/**
 * Cent-exact sum of dollar amounts. Replaces float `reduce((s, x) => s + x)`
 * chains, which accumulate binary representation error.
 *
 * sumMoney([0.1, 0.2])                      → 0.3 (not 0.30000000000000004)
 * sumMoney(rows, r => r.amount)             → picks a field
 */
function sumMoney(values, pick) {
  let total = 0;
  for (const v of values) {
    const raw = pick ? pick(v) : v;
    const cents = toCents(raw);
    if (cents !== null && !Number.isNaN(cents)) total += cents;
  }
  return total / 100;
}

/**
 * Multiply a dollar amount by a factor (interest rate, proration, etc.),
 * rounding the result to the nearest cent.
 */
function mulMoney(dollars, factor) {
  const cents = toCents(dollars);
  if (cents === null || Number.isNaN(cents) || !Number.isFinite(factor)) return 0;
  return Math.round(cents * factor) / 100;
}

const _usd = new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });

/** Dollar number → "$1,234.56" (negatives as "-$1,234.56"). null/undefined → "$0.00". */
function formatUSD(dollars) {
  const n = Number(dollars) || 0;
  return (n < 0 ? '-' : '') + '$' + _usd.format(Math.abs(n));
}

/** Integer cents → "$1,234.56". */
function formatCentsUSD(cents) {
  return formatUSD(fromCents(cents) ?? 0);
}

module.exports = { toCents, fromCents, roundMoney, sumMoney, mulMoney, formatUSD, formatCentsUSD };
