/**
 * Shared financial math for PRUDENTIA.
 * Single source of truth so the dashboard, analytics and goals never disagree.
 *
 * Convention: `profile.savings` is the BASELINE balance before any recorded
 * transaction. Net worth therefore equals baseline + net of ALL transactions,
 * and the net-worth series starts at the baseline and walks forward.
 */

export type TxLike = {
  amount: number | string;
  type: string;
  occurred_at: string | Date;
  category?: string | null;
};

export type Point = { t: number; value: number };

const num = (v: unknown) => {
  const n = Number(v);
  return Number.isFinite(n) ? n : 0;
};

export function signedAmount(t: TxLike) {
  return (t.type === "income" ? 1 : -1) * num(t.amount);
}

/** Net of all transactions (income positive, expense negative). */
export function totalNet(txs: TxLike[]) {
  return txs.reduce((s, t) => s + signedAmount(t), 0);
}

export function isSameMonth(date: string | Date, ref = new Date()) {
  const d = typeof date === "string" ? new Date(date) : date;
  return d.getMonth() === ref.getMonth() && d.getFullYear() === ref.getFullYear();
}

export function monthTotals(txs: TxLike[], ref = new Date()) {
  let earned = 0, spent = 0;
  for (const t of txs) {
    if (!isSameMonth(t.occurred_at, ref)) continue;
    if (t.type === "income") earned += num(t.amount);
    else spent += num(t.amount);
  }
  return { earned, spent, net: earned - spent };
}

/** Headline net worth — baseline plus every recorded transaction. */
export function netWorth(baseline: number, txs: TxLike[]) {
  return num(baseline) + totalNet(txs);
}

/** Chronological balance walk; final point always equals netWorth(). */
export function netWorthSeries(baseline: number, txs: TxLike[]): Point[] {
  if (!txs.length) return [];
  const sorted = [...txs].sort(
    (a, b) => new Date(a.occurred_at).getTime() - new Date(b.occurred_at).getTime(),
  );
  let bal = num(baseline);
  return sorted.map((t) => {
    bal += signedAmount(t);
    return { t: new Date(t.occurred_at).getTime(), value: Math.round(bal) };
  });
}

/** Observed monthly surplus over a trailing window (defaults to 90 days). */
export function observedMonthlySurplus(txs: TxLike[], days = 90, now = Date.now()) {
  const cutoff = now - days * 86_400_000;
  let net = 0, count = 0;
  for (const t of txs) {
    const ts = new Date(t.occurred_at).getTime();
    if (ts < cutoff || ts > now) continue;
    count++;
    net += signedAmount(t);
  }
  if (!count) return 0;
  return Math.max(0, net / (days / 30.44));
}

/** Fraction of the current month elapsed, in (0, 1]. */
export function monthProgress(now = new Date()) {
  const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
  return now.getDate() / daysInMonth;
}

/** Straight-line projection of month-end spend from spend-to-date. */
export function projectedSpend(spentToDate: number, now = new Date()) {
  const p = monthProgress(now);
  return p > 0 ? spentToDate / p : 0;
}

/** Months required to close a gap at a given monthly rate. */
export function monthsToGoal(remaining: number, perMonth: number) {
  if (remaining <= 0) return 0;
  if (perMonth <= 0) return Infinity;
  return Math.ceil(remaining / perMonth);
}

/** Deterministic likelihood of hitting a deadline at the current run-rate. */
export function goalProbability(
  remaining: number,
  perMonth: number,
  targetDate?: string | Date | null,
  now = Date.now(),
): number | null {
  if (!targetDate) return null;
  if (remaining <= 0) return 100;
  if (perMonth <= 0) return 0;
  const monthsLeft = (new Date(targetDate).getTime() - now) / (30.44 * 86_400_000);
  if (monthsLeft <= 0) return 0;
  const ratio = (perMonth * monthsLeft) / remaining;
  const p = 100 / (1 + Math.exp(-6 * (ratio - 1)));
  return Math.round(Math.min(99, Math.max(1, p)));
}
