import type { Dollars } from '../utils/money.mts';

const { roundMoney, sumMoney } =
  require('../utils/money.mts') as typeof import('../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.
 */

type Row = Record<string, any>;

interface ActiveDebt {
  id: any;
  name: any;
  balance: number;
  minPayment: number;
  monthlyRate: number;
  payoffMonth: number | null;
  totalInterest: number;
}
interface SkippedDebt {
  id: any;
  name: any;
  reason: string;
}
interface DebtProjection {
  months_to_freedom: number | null;
  total_interest_paid: number;
  payoff_date: string | null;
  payoff_display: string | null;
  debts: any[];
  skipped: SkippedDebt[];
  extra_payment: number;
  capped: boolean;
}

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

function _simulate(orderedDebts: Row[], extraPayment: number, startDate: Date): DebtProjection {
  const extra = Math.max(0, Number(extraPayment) || 0);

  const active: ActiveDebt[] = [];
  const skipped: SkippedDebt[] = [];

  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: number): string {
    const d = new Date(baseYear, baseMo + m, 1);
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
  }

  function monthDisplay(m: number): string {
    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: Row[],
  extraPayment = 0,
  startDate: Date = new Date(),
): DebtProjection {
  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: Row[],
  extraPayment = 0,
  startDate: Date = new Date(),
): DebtProjection {
  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: number): Dollars {
  return roundMoney(n);
}

module.exports = { calculateSnowball, calculateAvalanche };
