
const { roundMoney, sumMoney } = require('../utils/money.mts');
/**
 * Debt payoff calculators — Snowball and Avalanche methods.
 *
 * Snowball  (Dave Ramsey): smallest balance first — fast psychological wins.
 * Avalanche (math-optimal): highest interest rate first — minimises total interest.
 *
 * Both share the same month-by-month simulation loop; only the initial order differs.
 */

// ── Private simulation engine ─────────────────────────────────────────────────

function _simulate(orderedDebts, extraPayment, startDate) {
  const extra = Math.max(0, Number(extraPayment) || 0);

  const active  = [];
  const skipped = [];

  for (const d of orderedDebts) {
    const bal = Number(d.current_balance);
    if (d.current_balance == null || !Number.isFinite(bal)) {
      skipped.push({ id: d.id, name: d.name, reason: 'no_balance' });
    } else if (bal <= 0) {
      skipped.push({ id: d.id, name: d.name, reason: 'zero_balance' });
    } else {
      active.push({
        id:            d.id,
        name:          d.name,
        balance:       bal,
        minPayment:    Math.max(0, Number(d.minimum_payment) || 0),
        monthlyRate:   Math.max(0, Number(d.interest_rate)   || 0) / 100 / 12,
        payoffMonth:   null,
        totalInterest: 0,
      });
    }
  }

  if (active.length === 0) {
    return {
      months_to_freedom:   null,
      total_interest_paid: 0,
      payoff_date:         null,
      payoff_display:      null,
      debts:               [],
      skipped,
      extra_payment:       extra,
      capped:              false,
    };
  }

  // ── Month-by-month loop ───────────────────────────────────────────────────
  const MAX_MONTHS = 600; // 50-year safety cap
  let rollingExtra = extra;
  let month = 0;

  while (active.some(d => d.balance > 0) && month < MAX_MONTHS) {
    month++;

    // Attack target = first debt in the ordered list that still has a balance
    const targetIdx = active.findIndex(d => d.balance > 0);

    for (let i = 0; i < active.length; i++) {
      const debt = active[i];
      if (debt.balance <= 0) continue;

      // Accrue monthly interest
      const interest = debt.balance * debt.monthlyRate;
      debt.balance      += interest;
      debt.totalInterest += interest;

      // Attack target gets minimums + full snowball; others get minimums only
      const payment = Math.min(
        debt.balance,
        i === targetIdx ? debt.minPayment + rollingExtra : debt.minPayment,
      );
      debt.balance = Math.max(0, debt.balance - payment);
      if (debt.balance < 0.005) debt.balance = 0; // eliminate floating-point dust
    }

    // Mark any debt that just reached zero (attack target OR paid off naturally by minimums)
    // and roll its freed minimum into the snowball for next month.
    for (let i = 0; i < active.length; i++) {
      const debt = active[i];
      if (debt.balance === 0 && debt.payoffMonth === null) {
        debt.payoffMonth = month;
        rollingExtra += debt.minPayment;
      }
    }
  }

  // ── Format results ────────────────────────────────────────────────────────
  const baseYear = startDate.getFullYear();
  const baseMo   = startDate.getMonth();

  function monthLabel(m) {
    const d = new Date(baseYear, baseMo + m, 1);
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
  }

  function monthDisplay(m) {
    const d = new Date(baseYear, baseMo + m, 1);
    return d.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
  }

  const debtResults = active.map(d => ({
    id:             d.id,
    name:           d.name,
    payoff_month:   d.payoffMonth,
    payoff_date:    d.payoffMonth ? monthLabel(d.payoffMonth)   : null,
    payoff_display: d.payoffMonth ? monthDisplay(d.payoffMonth) : null,
    total_interest: round2(d.totalInterest),
    months:         d.payoffMonth,
    never_paid:     d.payoffMonth === null, // minimum never overcomes interest
  }));

  // A payoff date only exists if EVERY active debt actually reaches $0. If a
  // debt's minimum never overcomes its interest (and the snowball can't cover
  // it), it never pays off — reporting the other debts' last month would be a
  // misleadingly optimistic "freedom" date, so return null and mark it capped.
  const allPaid       = active.every(d => d.payoffMonth !== null);
  const maxMonth      = Math.max(0, ...active.map(d => d.payoffMonth || 0));
  const totalInterest = sumMoney(active, d => d.totalInterest);

  return {
    months_to_freedom:   allPaid && maxMonth ? maxMonth : null,
    total_interest_paid: round2(totalInterest),
    payoff_date:         allPaid && maxMonth ? monthLabel(maxMonth)   : null,
    payoff_display:      allPaid && maxMonth ? monthDisplay(maxMonth) : null,
    debts:               debtResults,
    skipped,
    extra_payment:       extra,
    capped:              month >= MAX_MONTHS || !allPaid,
  };
}

// ── Public API ────────────────────────────────────────────────────────────────

/**
 * Snowball: attack the smallest balance first (fast wins, motivational).
 * Debts must already be in snowball order (sorted by current_balance ASC by the caller).
 */
function calculateSnowball(debts, extraPayment = 0, startDate = new Date()) {
  return _simulate(debts, extraPayment, startDate);
}

/**
 * Avalanche: attack the highest interest rate first (minimises total interest paid).
 * Re-sorts debts internally — caller does not need to pre-sort.
 */
function calculateAvalanche(debts, extraPayment = 0, startDate = new Date()) {
  const sorted = [...debts].sort((a, b) => {
    const ra = Number(a.interest_rate) || 0;
    const rb = Number(b.interest_rate) || 0;
    if (rb !== ra) return rb - ra; // highest rate first
    // Tiebreak: smallest balance (clears fastest, rolling the payment sooner)
    return (Number(a.current_balance) || 0) - (Number(b.current_balance) || 0);
  });
  return _simulate(sorted, extraPayment, startDate);
}

function round2(n) {
  return roundMoney(n);
}

module.exports = { calculateSnowball, calculateAvalanche };
