/**
 * PRUDENTIA — shared financial intelligence layer.
 *
 * Everything here is pure and deterministic: financial state, forecasting,
 * scenario projection, the Prudence Score rubric, alerts and next best action.
 * The AI never invents numbers — it only phrases what these functions compute.
 *
 * Builds on src/lib/finance.ts so dashboards, decisions and AI all agree.
 */

import {
  netWorth,
  monthTotals,
  observedMonthlySurplus,
  monthsToGoal,
  goalProbability,
  isSameMonth,
  type TxLike,
} from "./finance";
import { formatCurrency } from "./format";

const MONTH_MS = 30.44 * 86_400_000;
const WEEKS_PER_MONTH = 4.345;

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

const clamp = (v: number, lo = 0, hi = 100) => Math.max(lo, Math.min(hi, v));

/* ------------------------------ financial state ----------------------------- */

export type GoalLike = {
  id?: string;
  name: string;
  target_amount: number | string;
  current_amount: number | string;
  target_date?: string | null;
  monthly_contribution?: number | string | null;
  priority?: number | null;
  category?: string | null;
};

export type ProfileLike = {
  currency?: string | null;
  monthly_income?: number | string | null;
  savings?: number | string | null;
  risk_tolerance?: string | null;
  preferences?: Record<string, unknown> | null;
};

export type GoalState = {
  id?: string;
  name: string;
  target: number;
  current: number;
  remaining: number;
  perMonth: number;
  monthsToGoal: number;
  projectedDate: number | null;
  targetDate: number | null;
  driftWeeks: number | null;
  odds: number | null;
  priority: number;
};

export type FinancialState = {
  currency: string;
  /** Baseline savings plus every recorded transaction. */
  netWorth: number;
  /** Liquid cash available — same as net worth until asset tracking exists. */
  cash: number;
  monthlyIncome: number;
  /** Trailing 90-day average monthly spend. */
  monthlyExpenses: number;
  monthlySurplus: number;
  savingsRate: number;
  reserveMonths: number;
  reserveTargetMonths: number;
  reserveTargetAmount: number;
  excessCash: number;
  thisMonth: { earned: number; spent: number; net: number };
  goals: GoalState[];
  primaryGoal: GoalState | null;
  assumptions: string[];
  hasData: boolean;
};

export function buildFinancialState(
  profile: ProfileLike | null | undefined,
  txs: TxLike[],
  goals: GoalLike[] = [],
  now = Date.now(),
): FinancialState {
  const p = profile ?? {};
  const currency = (p.currency as string) ?? "USD";
  const baseline = num(p.savings);
  const worth = netWorth(baseline, txs);
  const month = monthTotals(txs, new Date(now));
  const surplus = observedMonthlySurplus(txs, 90, now);

  // Trailing 90-day expense run-rate; falls back to this month's spend.
  const cutoff = now - 90 * 86_400_000;
  let spend90 = 0;
  let counted = 0;
  for (const t of txs) {
    const ts = new Date(t.occurred_at).getTime();
    if (ts < cutoff || ts > now) continue;
    if (t.type !== "income") { spend90 += num(t.amount); counted++; }
  }
  const monthlyExpenses = counted ? spend90 / (90 / 30.44) : month.spent;

  const incomeObserved = (() => {
    let inc = 0;
    for (const t of txs) {
      const ts = new Date(t.occurred_at).getTime();
      if (ts < cutoff || ts > now) continue;
      if (t.type === "income") inc += num(t.amount);
    }
    return counted ? inc / (90 / 30.44) : 0;
  })();
  const monthlyIncome = num(p.monthly_income) || incomeObserved;

  const prefs = (p.preferences ?? {}) as Record<string, unknown>;
  const reserveTargetMonths = num(prefs.reserve_months, 6) || 6;
  const reserveTargetAmount = monthlyExpenses * reserveTargetMonths;
  const reserveMonths = monthlyExpenses > 0 ? worth / monthlyExpenses : worth > 0 ? Infinity : 0;

  const goalStates: GoalState[] = goals.map((g) => {
    const target = num(g.target_amount);
    const current = num(g.current_amount);
    const remaining = Math.max(0, target - current);
    const perMonth = num(g.monthly_contribution) || surplus;
    const months = monthsToGoal(remaining, perMonth);
    const targetDate = g.target_date ? new Date(g.target_date).getTime() : null;
    const projectedDate = Number.isFinite(months) ? now + months * MONTH_MS : null;
    const driftWeeks =
      targetDate && projectedDate ? Math.round((projectedDate - targetDate) / (7 * 86_400_000)) : null;
    return {
      id: g.id,
      name: g.name,
      target,
      current,
      remaining,
      perMonth,
      monthsToGoal: months,
      projectedDate,
      targetDate,
      driftWeeks,
      odds: goalProbability(remaining, perMonth, g.target_date ?? null, now),
      priority: num(g.priority, 2),
    };
  });

  // Primary goal: soonest deadline, else highest priority (1 = highest).
  const primaryGoal =
    [...goalStates].sort((a, b) => {
      if (a.targetDate && b.targetDate) return a.targetDate - b.targetDate;
      if (a.targetDate) return -1;
      if (b.targetDate) return 1;
      return a.priority - b.priority;
    })[0] ?? null;

  const assumptions = [
    `Monthly spend estimated at ${Math.round(monthlyExpenses)} from your last 90 days of transactions.`,
    `Monthly surplus estimated at ${Math.round(surplus)} from the same window.`,
    `Emergency reserve target set to ${reserveTargetMonths} months of expenses.`,
    "Available cash is treated as your net worth; investments and property are not separated yet.",
  ];

  return {
    currency,
    netWorth: Math.round(worth),
    cash: Math.round(worth),
    monthlyIncome: Math.round(monthlyIncome),
    monthlyExpenses: Math.round(monthlyExpenses),
    monthlySurplus: Math.round(surplus),
    savingsRate: monthlyIncome > 0 ? Math.round((surplus / monthlyIncome) * 100) : 0,
    reserveMonths: Number.isFinite(reserveMonths) ? Math.round(reserveMonths * 10) / 10 : 0,
    reserveTargetMonths,
    reserveTargetAmount: Math.round(reserveTargetAmount),
    excessCash: Math.round(worth - reserveTargetAmount),
    thisMonth: { earned: Math.round(month.earned), spent: Math.round(month.spent), net: Math.round(month.net) },
    goals: goalStates,
    primaryGoal,
    assumptions,
    hasData: txs.length > 0,
  };
}

/* -------------------------------- scenarios -------------------------------- */

export type ScenarioSpec = {
  id: string;
  label: string;
  /** Upfront cash cost of this option. */
  amount: number;
  /** Months to wait before paying it. */
  waitMonths?: number;
  /** Ongoing monthly cost this option adds. */
  monthlyCost?: number;
  /** Treat the unspent money as saved/invested instead. */
  investInstead?: boolean;
};

export type ScenarioOutcome = {
  id: string;
  label: string;
  amount: number;
  waitMonths: number;
  monthlyCost: number;
  cashAfter: number;
  reserveMonthsAfter: number;
  surplusAfter: number;
  netWorth12m: number;
  goalDelayWeeks: number;
  goalProjectedDate: number | null;
};

const ANNUAL_RETURN = 0.07;

/** Project one option 12 months out against the current financial state. */
export function projectScenario(state: FinancialState, spec: ScenarioSpec, now = Date.now()): ScenarioOutcome {
  const amount = Math.max(0, num(spec.amount));
  const waitMonths = Math.max(0, num(spec.waitMonths));
  const monthlyCost = Math.max(0, num(spec.monthlyCost));

  // Cash saved up during the wait before the outlay lands.
  const cashAtPurchase = state.cash + state.monthlySurplus * waitMonths;
  const cashAfter = Math.round(cashAtPurchase - amount);
  const surplusAfter = Math.round(state.monthlySurplus - monthlyCost);
  const reserveMonthsAfter =
    state.monthlyExpenses > 0 ? Math.round((cashAfter / state.monthlyExpenses) * 10) / 10 : 0;

  // 12-month net worth walk with monthly compounding on the liquid balance.
  let balance = state.cash;
  const r = ANNUAL_RETURN / 12;
  for (let m = 1; m <= 12; m++) {
    if (m === waitMonths + 1 || (waitMonths === 0 && m === 1)) balance -= amount;
    const flow = state.monthlySurplus - (m > waitMonths ? monthlyCost : 0);
    balance = (balance + flow) * (1 + r);
  }
  if (spec.investInstead) {
    // Nothing extra: not spending already leaves the money compounding above.
  }

  const goal = state.primaryGoal;
  let goalDelayWeeks = 0;
  let goalProjectedDate: number | null = null;
  if (goal && goal.remaining > 0) {
    const perMonthAfter = Math.max(0, goal.perMonth - monthlyCost);
    const baseMonths = monthsToGoal(goal.remaining, goal.perMonth);
    // The upfront amount is money that would otherwise have fed the goal.
    const monthsLostToOutlay = goal.perMonth > 0 ? amount / goal.perMonth : amount > 0 ? Infinity : 0;
    const afterMonths = monthsToGoal(goal.remaining, perMonthAfter) + monthsLostToOutlay;
    const delta = Number.isFinite(afterMonths) && Number.isFinite(baseMonths) ? afterMonths - baseMonths : 0;
    goalDelayWeeks = Math.round(Math.max(0, delta) * WEEKS_PER_MONTH);
    goalProjectedDate = Number.isFinite(afterMonths) ? now + afterMonths * MONTH_MS : null;
  }

  return {
    id: spec.id,
    label: spec.label,
    amount,
    waitMonths,
    monthlyCost,
    cashAfter,
    reserveMonthsAfter,
    surplusAfter,
    netWorth12m: Math.round(balance),
    goalDelayWeeks,
    goalProjectedDate,
  };
}

/** The four standard options every purchase decision is compared against. */
export function standardScenarios(amount: number, monthlyCost = 0): ScenarioSpec[] {
  return [
    { id: "now", label: "Buy now", amount, waitMonths: 0, monthlyCost },
    { id: "wait", label: "Wait 3 months", amount, waitMonths: 3, monthlyCost },
    { id: "cheaper", label: "Cheaper alternative", amount: Math.round(amount * 0.6), waitMonths: 0, monthlyCost: Math.round(monthlyCost * 0.6) },
    { id: "skip", label: "Don't buy, save it", amount: 0, waitMonths: 0, monthlyCost: 0, investInstead: true },
  ];
}

/* ------------------------------ prudence score ------------------------------ */

export type ScoreComponent = {
  key: string;
  label: string;
  score: number;
  weight: number;
  detail: string;
};

export type Verdict = {
  score: number;
  label: "PRUDENT" | "ACCEPTABLE" | "STRETCHED" | "IMPRUDENT";
  action: "BUY" | "WAIT" | "SPLIT" | "DONT";
  headline: string;
  components: ScoreComponent[];
  betterOption: ScenarioOutcome | null;
  scenarios: ScenarioOutcome[];
  assumptions: string[];
  changesTheAnswer: string;
};

const round = (n: number) => Math.round(n);

/**
 * Rubric-based Prudence Score. Every component is derived from real figures,
 * so the total is always traceable back to the user's data.
 */
export function scoreDecision(
  state: FinancialState,
  amount: number,
  monthlyCost = 0,
  now = Date.now(),
): Verdict {
  const scenarios = standardScenarios(amount, monthlyCost).map((s) => projectScenario(state, s, now));
  const buyNow = scenarios[0];
  const wait = scenarios[1];
  const cheaper = scenarios[2];

  // 1. Affordability — how much cash is left relative to a month of expenses.
  const monthsLeft = state.monthlyExpenses > 0 ? buyNow.cashAfter / state.monthlyExpenses : buyNow.cashAfter > 0 ? 12 : 0;
  const affordability = clamp((monthsLeft / 3) * 100);

  // 2. Emergency reserve — after vs target.
  const reserveRatio = state.reserveTargetMonths > 0 ? buyNow.reserveMonthsAfter / state.reserveTargetMonths : 1;
  const reserve = clamp(reserveRatio * 100);

  // 3. Cash flow — surplus remaining after any ongoing cost.
  const cashflow = state.monthlySurplus > 0
    ? clamp((buyNow.surplusAfter / state.monthlySurplus) * 100)
    : monthlyCost > 0 ? 0 : 55;

  // 4. Goal impact — weeks of delay to the nearest goal.
  const goalScore = state.primaryGoal ? clamp(100 - buyNow.goalDelayWeeks * 6) : 70;

  // 5. Opportunity cost — what the money becomes in 5 years at 7%.
  const fiveYear = amount * Math.pow(1 + ANNUAL_RETURN, 5);
  const oppRatio = state.monthlySurplus > 0 ? amount / (state.monthlySurplus * 12) : amount > 0 ? 2 : 0;
  const opportunity = clamp(100 - oppRatio * 60);

  // 6. Risk — thin reserve plus recurring commitment is the risky combination.
  const risk = clamp(
    100 - (monthlyCost > 0 ? 25 : 0) - (buyNow.reserveMonthsAfter < 3 ? 35 : 0) - (buyNow.cashAfter < 0 ? 40 : 0),
  );

  // 7. Long-term impact — 12-month net worth versus not buying.
  const skip = scenarios[3];
  const ltRatio = skip.netWorth12m !== 0 ? buyNow.netWorth12m / skip.netWorth12m : 1;
  const longTerm = clamp(50 + (ltRatio - 1) * 200);

  const cur = state.currency;
  const money = (n: number) => formatCurrency(Math.round(n), cur, 0);

  const components: ScoreComponent[] = [
    {
      key: "affordability", label: "Affordability", score: round(affordability), weight: 0.22,
      detail: `${money(buyNow.cashAfter)} left after paying, about ${monthsLeft.toFixed(1)} months of your expenses.`,
    },
    {
      key: "reserve", label: "Emergency reserve", score: round(reserve), weight: 0.2,
      detail: `Reserve moves from ${state.reserveMonths} to ${buyNow.reserveMonthsAfter} months against your ${state.reserveTargetMonths}-month target.`,
    },
    {
      key: "cashflow", label: "Cash flow", score: round(cashflow), weight: 0.15,
      detail: monthlyCost > 0
        ? `Monthly surplus falls from ${money(state.monthlySurplus)} to ${money(buyNow.surplusAfter)}.`
        : `No ongoing cost, so your ${money(state.monthlySurplus)} monthly surplus is unchanged.`,
    },
    {
      key: "goal", label: "Goal impact", score: round(goalScore), weight: 0.18,
      detail: state.primaryGoal
        ? buyNow.goalDelayWeeks > 0
          ? `Pushes "${state.primaryGoal.name}" back about ${buyNow.goalDelayWeeks} weeks.`
          : `No measurable delay to "${state.primaryGoal.name}".`
        : "No active goal with a deadline to measure against.",
    },
    {
      key: "opportunity", label: "Opportunity cost", score: round(opportunity), weight: 0.1,
      detail: `Estimated ${money(fiveYear)} in 5 years if invested instead, assuming 7% a year.`,
    },
    {
      key: "risk", label: "Risk", score: round(risk), weight: 0.08,
      detail: buyNow.cashAfter < 0
        ? "You do not currently have the cash for this."
        : buyNow.reserveMonthsAfter < 3
          ? "Leaves your safety buffer under 3 months."
          : "Keeps a workable safety buffer.",
    },
    {
      key: "long_term", label: "Long-term impact", score: round(longTerm), weight: 0.07,
      detail: `12-month projected balance ${money(buyNow.netWorth12m)} versus ${money(skip.netWorth12m)} if you skip it.`,
    },
  ];

  const total = round(components.reduce((s, c) => s + c.score * c.weight, 0));

  const label: Verdict["label"] =
    total >= 75 ? "PRUDENT" : total >= 60 ? "ACCEPTABLE" : total >= 42 ? "STRETCHED" : "IMPRUDENT";

  // Is waiting or a cheaper option materially better?
  const waitScore = scoreLite(state, wait);
  const cheaperScore = scoreLite(state, cheaper);
  const nowScore = scoreLite(state, buyNow);
  let betterOption: ScenarioOutcome | null = null;
  if (waitScore > nowScore + 8) betterOption = wait;
  if (cheaperScore > Math.max(nowScore, waitScore) + 8) betterOption = cheaper;

  const action: Verdict["action"] =
    total < 42 ? "DONT" : betterOption?.id === "wait" ? "WAIT" : betterOption?.id === "cheaper" ? "SPLIT" : total >= 60 ? "BUY" : "WAIT";

  const headline =
    action === "BUY"
      ? `You can do this without compromising your reserve or your current goals.`
      : action === "WAIT"
        ? `Affordable, but waiting keeps your safety buffer and goal timeline intact.`
        : action === "SPLIT"
          ? `A smaller version of this purchase fits your position much better.`
          : `This would stretch your finances further than is sensible right now.`;

  const changesTheAnswer =
    action === "BUY"
      ? `A drop in income or a rise in monthly spending above ${money(state.monthlyExpenses * 1.15)} would change this.`
      : buyNow.reserveMonthsAfter < state.reserveTargetMonths
        ? `Reaching ${money(state.reserveTargetAmount)} in reserve first would change this to a clear yes.`
        : `Reducing the amount or spreading it over time would change this.`;

  return {
    score: total,
    label,
    action,
    headline,
    components,
    betterOption,
    scenarios,
    assumptions: state.assumptions,
    changesTheAnswer,
  };
}

/** Compact comparison score used only to rank scenarios against each other. */
function scoreLite(state: FinancialState, o: ScenarioOutcome) {
  const monthsLeft = state.monthlyExpenses > 0 ? o.cashAfter / state.monthlyExpenses : 6;
  const reserve = state.reserveTargetMonths > 0 ? (o.reserveMonthsAfter / state.reserveTargetMonths) * 100 : 100;
  const goal = 100 - o.goalDelayWeeks * 6;
  const patience = o.waitMonths * 4; // waiting has a real cost in delay/utility
  return clamp((clamp((monthsLeft / 3) * 100) + clamp(reserve) + clamp(goal)) / 3 - patience);
}

/* --------------------------------- alerts ---------------------------------- */

export type Alert = {
  key: string;
  title: string;
  body: string;
  severity: "info" | "success" | "warning" | "critical";
  action?: string;
};

/** Deliberately quiet: only meaningful, number-backed alerts. */
export function detectAlerts(state: FinancialState, txs: TxLike[], now = Date.now()): Alert[] {
  const out: Alert[] = [];
  const cur = state.currency;
  const money = (n: number) => formatCurrency(Math.round(n), cur, 0);
  if (!state.hasData) return out;

  // Goal drift
  for (const g of state.goals) {
    if (g.driftWeeks != null && g.driftWeeks >= 3) {
      out.push({
        key: `goal-drift:${g.id ?? g.name}`,
        title: `${g.name} is drifting`,
        body: `At your current rate of ${money(g.perMonth)} a month, this goal is projected about ${g.driftWeeks} weeks behind its target date.`,
        severity: g.driftWeeks >= 12 ? "critical" : "warning",
        action: `Add ${money(Math.max(0, g.remaining / Math.max(1, (g.targetDate! - now) / MONTH_MS)) - g.perMonth)} a month to get back on schedule.`,
      });
    } else if (g.driftWeeks != null && g.driftWeeks <= -4) {
      out.push({
        key: `goal-ahead:${g.id ?? g.name}`,
        title: `${g.name} is ahead of plan`,
        body: `You are projected to reach this goal about ${Math.abs(g.driftWeeks)} weeks early.`,
        severity: "success",
      });
    }
  }

  // Reserve below target
  if (state.monthlyExpenses > 0 && state.reserveMonths < state.reserveTargetMonths) {
    out.push({
      key: "reserve-low",
      title: "Emergency reserve below target",
      body: `You hold ${state.reserveMonths} months of expenses against a ${state.reserveTargetMonths}-month target — a shortfall of ${money(Math.max(0, state.reserveTargetAmount - state.cash))}.`,
      severity: state.reserveMonths < 3 ? "critical" : "warning",
    });
  }

  // Excess cash above buffer
  if (state.excessCash > state.monthlyExpenses) {
    out.push({
      key: "excess-cash",
      title: "Cash above your planned buffer",
      body: `You are holding ${money(state.excessCash)} more than your ${state.reserveTargetMonths}-month buffer needs.`,
      severity: "info",
      action: state.primaryGoal ? `Move part of it toward ${state.primaryGoal.name}.` : "Consider putting it to work.",
    });
  }

  // Discretionary spending shift: this month vs trailing average
  const spentThisMonth = state.thisMonth.spent;
  const elapsed = new Date(now).getDate() / new Date(new Date(now).getFullYear(), new Date(now).getMonth() + 1, 0).getDate();
  const projected = elapsed > 0 ? spentThisMonth / elapsed : 0;
  if (state.monthlyExpenses > 0 && projected > state.monthlyExpenses * 1.15 && elapsed > 0.25) {
    const pct = Math.round((projected / state.monthlyExpenses - 1) * 100);
    out.push({
      key: "spend-up",
      title: "Spending is running high",
      body: `This month is on track for ${money(projected)}, about ${pct}% above your 90-day average of ${money(state.monthlyExpenses)}.`,
      severity: "warning",
    });
  } else if (state.monthlyExpenses > 0 && projected > 0 && projected < state.monthlyExpenses * 0.85 && elapsed > 0.5) {
    out.push({
      key: "spend-down",
      title: "Spending is down this month",
      body: `On track for ${money(projected)} versus your usual ${money(state.monthlyExpenses)}.`,
      severity: "success",
    });
  }

  return out;
}

/* ----------------------------- next best action ----------------------------- */

export type NextAction = { title: string; why: string; effect: string } | null;

export function nextBestAction(state: FinancialState, alerts: Alert[]): NextAction {
  const cur = state.currency;
  const money = (n: number) => formatCurrency(Math.round(n), cur, 0);
  if (!state.hasData) {
    return {
      title: "Add your first transactions",
      why: "Prudentia needs a few weeks of real spending before it can judge your decisions.",
      effect: "Unlocks your verdicts, alerts and forecasts.",
    };
  }

  if (state.reserveMonths < 3 && state.monthlySurplus > 0) {
    const amt = Math.min(state.monthlySurplus, Math.max(0, state.reserveTargetAmount - state.cash));
    return {
      title: `Move ${money(amt)} into your emergency reserve`,
      why: `Your reserve covers ${state.reserveMonths} months, below the 3-month floor.`,
      effect: `Lifts your buffer toward the ${state.reserveTargetMonths}-month target without touching your goals.`,
    };
  }

  const drifting = state.goals.find((g) => g.driftWeeks != null && g.driftWeeks >= 3);
  if (drifting && drifting.targetDate) {
    const monthsLeft = Math.max(1, (drifting.targetDate - Date.now()) / MONTH_MS);
    const needed = drifting.remaining / monthsLeft;
    return {
      title: `Raise ${drifting.name} to ${money(needed)} a month`,
      why: `At ${money(drifting.perMonth)} a month it lands about ${drifting.driftWeeks} weeks late.`,
      effect: "Brings the goal back onto its target date.",
    };
  }

  if (state.excessCash > state.monthlyExpenses && state.primaryGoal) {
    return {
      title: `Allocate ${money(Math.min(state.excessCash, state.primaryGoal.remaining))} to ${state.primaryGoal.name}`,
      why: `You hold ${money(state.excessCash)} above your planned cash buffer.`,
      effect: "Shortens your nearest goal without weakening your buffer.",
    };
  }

  const spendAlert = alerts.find((a) => a.key === "spend-up");
  if (spendAlert) {
    return { title: "Review this month's discretionary spending", why: spendAlert.body, effect: "Protects your monthly surplus before the month closes." };
  }

  return {
    title: "Hold your course",
    why: `Reserve at ${state.reserveMonths} months, surplus at ${money(state.monthlySurplus)} a month, no goal drifting.`,
    effect: "Nothing needs changing this week.",
  };
}

/** Convenience for AI prompts: everything the model is allowed to talk about. */
export function intelligenceSummary(state: FinancialState) {
  return {
    currency: state.currency,
    net_worth: state.netWorth,
    cash: state.cash,
    monthly_income: state.monthlyIncome,
    monthly_expenses: state.monthlyExpenses,
    monthly_surplus: state.monthlySurplus,
    savings_rate_pct: state.savingsRate,
    emergency_reserve_months: state.reserveMonths,
    emergency_reserve_target_months: state.reserveTargetMonths,
    excess_cash: state.excessCash,
    this_month: state.thisMonth,
    goals: state.goals.map((g) => ({
      name: g.name, target: g.target, current: g.current, remaining: g.remaining,
      per_month: g.perMonth, months_to_goal: Number.isFinite(g.monthsToGoal) ? g.monthsToGoal : null,
      drift_weeks: g.driftWeeks, on_time_odds: g.odds,
    })),
    assumptions: state.assumptions,
    note: "These figures are computed by Prudentia and match the app dashboards. Never recompute or invent figures.",
  };
}

export { isSameMonth };
