/**
 * Shared weekly finance logic for BaoLife.
 *
 * This is the single source of truth for recurring weekly income/expenses,
 * used by BOTH game-loop paths:
 *   - ONLINE players via PlayerSession.processWeekTick
 *   - OFFLINE players via GameEngine.handleFinances (LoopManager background job)
 *
 * The audit found the old economy was trivially solvable: a flat $50/wk expense
 * against $2000+/mo income meant money accumulated without bound and there were
 * no real sinks. This now activates the previously-dead spending model from
 * stats_manager (EXPENSE_MODIFIERS + SAVINGS_RATES) and introduces two scaling
 * sinks so income actually matters:
 *
 *   1. Scaling RENT: rent = clamp(weeklyIncome * 0.30, $50, $1250) instead of a
 *      flat $50. Higher earners owe more, throttling infinite accumulation.
 *   2. Lifestyle UPKEEP: owned prestige items carry a small recurring weekly
 *      `weeklyUpkeep`; the sum is charged every week.
 *
 * Lifestyle (frugal / normal / extravagant) scales total expenses via
 * EXPENSE_MODIFIERS and applies a discretionary-spend deduction on income via
 * SAVINGS_RATES (extravagant keeps less of each paycheck). Money is floored at 0.
 *
 * Finance is silent: no player-facing event is emitted, so no dedup key.
 */

import { Person } from '../models/index.js';
import {
  computeWeeklyRent,
  NON_EARNING_OCCUPATIONS,
  WEEKS_PER_MONTH,
} from './economyConstants.js';
import {
  getExpenseModifier,
  getSavingsRate,
  type SpendingHabit,
} from '../stats/stats_manager.js';

/**
 * Legacy flat base expense constant. Retained as the rent FLOOR so existing
 * imports keep working; the live path now uses the scaling rent in
 * computeWeeklyRent (whose floor is this value).
 */
export const WEEKLY_EXPENSES = 50;

/** A breakdown of a person's weekly finances, useful for tests and UI. */
export interface WeeklyFinanceBreakdown {
  /** Gross weekly income before any deductions (salary/4 for working roles). */
  grossIncome: number;
  /** Scaling rent sink before the lifestyle modifier. */
  rent: number;
  /** Summed weekly upkeep of all owned items, before the lifestyle modifier. */
  upkeep: number;
  /** Lifestyle expense modifier applied to rent + upkeep. */
  expenseModifier: number;
  /** Fixed sinks actually charged this week: (rent + upkeep) * modifier. */
  fixedExpenses: number;
  /** Income left after the fixed sinks (can be negative). */
  surplus: number;
  /** Discretionary lifestyle spend: positive surplus * (1 - savingsRate). */
  discretionarySpend: number;
  /** Net change to money this week, pre-floor. */
  net: number;
}

/**
 * Sum the recurring weekly upkeep of every item a person owns.
 * Items without a `weeklyUpkeep` contribute nothing.
 */
export function sumWeeklyUpkeep(person: Person): number {
  let total = 0;
  for (const item of person.items ?? []) {
    const upkeep = (item as { weeklyUpkeep?: number }).weeklyUpkeep;
    if (typeof upkeep === 'number' && upkeep > 0) {
      total += upkeep;
    }
  }
  return total;
}

/**
 * Compute (without mutating) the full weekly finance breakdown for a person.
 * Shared by both engines and by the live mutator below, so online == offline.
 */
export function computeWeeklyFinances(person: Person): WeeklyFinanceBreakdown {
  const lifestyle = (person.spendingHabits as SpendingHabit) ?? 'normal';
  const expenseModifier = getExpenseModifier(lifestyle);
  const savingsRate = getSavingsRate(lifestyle);

  // Income only accrues for working occupations (not students / preschoolers).
  const earns = !!person.occupation && !NON_EARNING_OCCUPATIONS.has(person.occupation);
  const grossIncome = earns ? (person.salary ?? 0) / WEEKS_PER_MONTH : 0;

  // Scaling rent sink + owned-item upkeep, both scaled by the lifestyle modifier
  // (EXPENSE_MODIFIERS: frugal 0.7, normal 1.0, extravagant 1.5).
  const rent = computeWeeklyRent(grossIncome);
  const upkeep = sumWeeklyUpkeep(person);
  const fixedExpenses = (rent + upkeep) * expenseModifier;

  // Income left after the fixed sinks. Can be negative (a low earner with a
  // pile of luxury upkeep, or a non-earner who still owes the rent floor).
  const surplus = grossIncome - fixedExpenses;

  // Of any positive surplus the person SAVES `savingsRate` (SAVINGS_RATES:
  // frugal 0.20, normal 0.10, extravagant 0.05) and spends the rest on
  // discretionary lifestyle. A negative surplus is a straight loss (no savings
  // to skim). This makes lifestyle change weekly NET cashflow:
  //   net = surplus > 0 ? surplus * savingsRate : surplus
  const discretionarySpend = surplus > 0 ? surplus * (1 - savingsRate) : 0;
  const net = surplus > 0 ? surplus * savingsRate : surplus;

  return {
    grossIncome,
    rent,
    upkeep,
    expenseModifier,
    fixedExpenses,
    surplus,
    discretionarySpend,
    net,
  };
}

/**
 * Apply one week of finances to a person, mutating `person.money` in place.
 *
 * Income accrues for working occupations; scaling rent, lifestyle upkeep, and a
 * discretionary lifestyle spend are charged. Resulting money is floored at 0.
 */
export function applyWeeklyFinances(person: Person): void {
  const { net } = computeWeeklyFinances(person);
  person.money = Math.max(0, (person.money ?? 0) + net);
}

/** In-game days per finance week (matches the Monday weekly-tick cadence). */
export const DAYS_PER_FINANCE_WEEK = 7;

/**
 * Apply a PRORATED slice of weekly finances for a partial week, mutating
 * `person.money` in place. `days` is the number of in-game days in the slice
 * (0..7); the weekly net is scaled by days/7.
 *
 * This is the offline-digest companion to the per-Monday weekly tick: the
 * online/offline loop charges a full week's net only when it crosses a Monday
 * boundary, so a multi-day absence that does NOT span a Monday would otherwise
 * yield a $0 money delta. Prorating the leftover (sub-week) days closes that
 * gap WITHOUT double-counting: callers prorate ONLY the days the loop's whole-
 * week ticks did not already cover. Money is floored at 0, same as the weekly
 * path, so a non-earner's rent floor produces a sensible negative-then-floored
 * delta and an earner produces a positive savings delta.
 */
export function applyProratedFinances(person: Person, days: number): void {
  const clampedDays = Math.max(0, Math.min(DAYS_PER_FINANCE_WEEK, days));
  if (clampedDays <= 0) return;
  const { net } = computeWeeklyFinances(person);
  const proratedNet = net * (clampedDays / DAYS_PER_FINANCE_WEEK);
  person.money = Math.max(0, (person.money ?? 0) + proratedNet);
}
