// Loan amortization engine.
//
// All-in-one runner: handles a fixed-rate loan that may have:
//   - financed fees (added to principal)
//   - amortization term ≥ balloon term (balloon = lump payoff at month N)
//   - optional extra monthly payment (applied to principal each month)
//
// Returns:
//   payment           : scheduled monthly P&I (sized to amort term)
//   schedule          : per-month rows for the bank's view (no extras)
//                       { month, interest, principalPaid, balance,
//                         cumPrincipal, cumInterest, cumPaid }
//   scheduleWithExtra : same shape, but with extra payment applied each month
//                       (truncates early when fully paid off)
//   balloon           : { month, balance } when balloon < amort, else null
//   totalsBank        : { totalPaid, totalInterest, payoffMonth }
//   totalsAccel       : { totalPaid, totalInterest, payoffMonth }
//
// Conventions:
//   - "month" is 1-indexed.
//   - "payoff month" for the bank track = balloon month if present, else amort month.
//   - "payoff month" for accel track = first month balance hits 0 (or balloon month
//     if balloon comes first; we model the borrower paying the balloon if needed).

function computeLoan({ loanAmount, financedFees, ratePct, amortYears, balloonYears, extraMonthly }) {
  const principal = Math.max(0, (Number(loanAmount) || 0) + (Number(financedFees) || 0));
  const r = (Number(ratePct) || 0) / 100 / 12;
  const amortN = Math.max(1, Math.round((Number(amortYears) || 0) * 12));
  const balloonN = Math.max(1, Math.round((Number(balloonYears) || 0) * 12));
  const extra = Math.max(0, Number(extraMonthly) || 0);
  const hasBalloon = balloonN < amortN;
  const payoffN = hasBalloon ? balloonN : amortN;

  // Scheduled monthly P&I (sized to amort term).
  let payment;
  if (principal <= 0) {
    payment = 0;
  } else if (r === 0) {
    payment = principal / amortN;
  } else {
    payment = principal * (r * Math.pow(1 + r, amortN)) / (Math.pow(1 + r, amortN) - 1);
  }

  // Bank schedule — only the rows up to payoff (balloon or final).
  const schedule = [];
  {
    let bal = principal;
    let cumP = 0, cumI = 0, cumPaid = 0;
    for (let m = 1; m <= payoffN; m++) {
      const interest = bal * r;
      let principalPaid = payment - interest;
      // Last regular month should clear the balance exactly (no balloon)
      if (!hasBalloon && m === amortN) {
        principalPaid = bal;
      }
      bal = Math.max(0, bal - principalPaid);
      cumP += principalPaid;
      cumI += interest;
      cumPaid += principalPaid + interest;
      schedule.push({
        month: m,
        interest, principalPaid,
        balance: bal,
        cumPrincipal: cumP, cumInterest: cumI, cumPaid,
      });
    }
  }

  // Balloon payoff (lump on the balloon month) — borrower owes the remaining balance.
  const balloon = hasBalloon
    ? { month: balloonN, balance: schedule[schedule.length - 1].balance }
    : null;

  // Bank totals (including balloon lump if any).
  const totalsBank = (() => {
    const last = schedule[schedule.length - 1];
    const lumpPaid = balloon ? balloon.balance : 0;
    return {
      totalPaid: (last?.cumPaid || 0) + lumpPaid,
      totalInterest: last?.cumInterest || 0,
      payoffMonth: payoffN,
      balloonLump: lumpPaid,
    };
  })();

  // Accelerated schedule — extra payment applied to principal each month.
  // Stops early when balance hits 0. If balloon arrives first, borrower still
  // pays remaining balance as a lump on that month (rare, but possible).
  const scheduleAccel = [];
  {
    let bal = principal;
    let cumP = 0, cumI = 0, cumPaid = 0;
    let m = 1;
    let paidOff = false;
    let payoffMonth = payoffN;
    let accelBalloonLump = 0;

    const maxRunM = Math.min(amortN, balloonN); // never run past the balloon
    while (m <= maxRunM) {
      const interest = bal * r;
      let principalPaid = payment - interest + extra;
      if (principalPaid > bal) principalPaid = bal; // last month
      const monthPaid = principalPaid + interest;
      bal = Math.max(0, bal - principalPaid);
      cumP += principalPaid;
      cumI += interest;
      cumPaid += monthPaid;
      scheduleAccel.push({
        month: m,
        interest, principalPaid,
        balance: bal,
        cumPrincipal: cumP, cumInterest: cumI, cumPaid,
      });
      if (bal <= 0.005) {
        paidOff = true;
        payoffMonth = m;
        break;
      }
      m++;
    }
    // If we hit balloon without paying off, pay the lump.
    if (!paidOff && hasBalloon) {
      accelBalloonLump = scheduleAccel[scheduleAccel.length - 1].balance;
      payoffMonth = balloonN;
    } else if (!paidOff) {
      payoffMonth = amortN;
    }

    // Snap final balance to 0 in the schedule for cleaner viz.
    if (paidOff) {
      const last = scheduleAccel[scheduleAccel.length - 1];
      last.balance = 0;
    }

    var totalsAccel = {
      totalPaid: cumPaid + accelBalloonLump,
      totalInterest: cumI,
      payoffMonth,
      balloonLump: accelBalloonLump,
      paidOff,
    };
  }

  // Loan name: "20 Due in 10 Balloon" / "10 Year"
  const loanName = (Number(amortYears) || 0) === (Number(balloonYears) || 0)
    ? `${Number(amortYears) || 0} Year`
    : `${Number(amortYears) || 0} Due in ${Number(balloonYears) || 0} Balloon`;

  return {
    principal,
    financedFees: Number(financedFees) || 0,
    payment,
    hasBalloon,
    balloon,
    schedule,
    scheduleAccel,
    totalsBank,
    totalsAccel,
    loanName,
  };
}

// APR-at-payoff curve (effective APR if borrower exits at month m).
//
// For each month m in the schedule, solve for the rate that satisfies:
//   netCashReceived = sum_{k=1..m} payment_k / (1 + apr/12)^k
//                   + payoffBalance_m / (1 + apr/12)^m
//
// netCashReceived is the actual cash the borrower received: loanAmount (NOT
// loanAmount + financedFees, since fees were withheld at close, even though
// they're amortized into the balance the bank tracks).
//
// We use Newton's method on the monthly rate, then annualize ×12.
function computeAPRCurve({ loanAmount, financedFees, ratePct, schedule, payment }) {
  const netCash = Math.max(1, Number(loanAmount) || 0);
  const fees = Math.max(0, Number(financedFees) || 0);
  const baseR = (Number(ratePct) || 0) / 100 / 12;

  if (fees <= 0 || schedule.length === 0) {
    // No fees — APR equals stated rate at every payoff. Return a flat curve.
    return schedule.map((s) => ({ month: s.month, apr: Number(ratePct) || 0 }));
  }

  // NPV(apr_monthly) — netCash; we want NPV - netCash = 0.
  const npv = (rm, m) => {
    if (rm <= -0.999) return Infinity;
    let pv = 0;
    for (let k = 1; k <= m; k++) {
      pv += payment / Math.pow(1 + rm, k);
    }
    const balanceAtM = schedule[m - 1].balance;
    pv += balanceAtM / Math.pow(1 + rm, m);
    return pv;
  };

  const points = [];
  for (const s of schedule) {
    const m = s.month;
    // Newton's method seeded at the stated rate.
    let rm = baseR > 0 ? baseR : 0.005;
    for (let iter = 0; iter < 60; iter++) {
      const f = npv(rm, m) - netCash;
      // numeric derivative
      const eps = 1e-7;
      const fPlus = npv(rm + eps, m) - netCash;
      const dF = (fPlus - f) / eps;
      if (!isFinite(dF) || Math.abs(dF) < 1e-12) break;
      const next = rm - f / dF;
      if (!isFinite(next)) break;
      rm = Math.max(-0.99 / 12, Math.min(2, next));
      if (Math.abs(f) < 0.01) break;
    }
    points.push({ month: m, apr: rm * 12 * 100 });
  }
  return points;
}

window.computeLoan = computeLoan;
window.computeAPRCurve = computeAPRCurve;
