/**
 * Stats Manager Module
 * Ported from Python stats/stats_manager.py
 *
 * Contains all stats and state update functions:
 * - checkDilemmas: Legacy dilemma polling (deprecated, kept for compatibility tests)
 * - checkEvents: Legacy event polling (deprecated, kept for compatibility tests)
 * - checkTutorialEvents: Legacy tutorial polling (deprecated, kept for compatibility tests)
 * - handleMoods: Update character moods based on stats
 * - handleFinances: Handle character finances and savings
 * - receiveSalary: Weekly salary with savings deduction based on spending habits
 * - calculateMonthlyExpenses: Monthly expense calculation based on spending habits
 * - getSavingsRate: Get savings rate for a spending habit
 * - getPeakEnergy: Calculate peak energy based on activities
 * - connect: Handle player reconnection and offline stats
 */

import { Player, Person } from '../models/index.js';

// ============================================================================
// Spending Habits and Finance Constants
// ============================================================================

/**
 * Spending habit types that affect savings rate and expenses
 * - frugal: Conservative spending, saves 20% of income
 * - normal: Balanced spending, saves 10% of income
 * - extravagant: High spending, saves only 5% of income
 */
export type SpendingHabit = 'frugal' | 'normal' | 'extravagant';

/**
 * Savings rates by spending habit type
 * These determine what percentage of income goes to savings vs spending
 */
// Fraction of weekly surplus BANKED as savings (the rest is discretionary
// lifestyle spend). Raised from 0.20/0.10/0.05 so a successful career builds
// meaningful wealth — at 10% even a lifelong CTO only banked ~$476k, leaving the
// entire aspirational shop tier ($300k-$5M) unreachable. At these rates a strong
// career banks into the low millions, putting most of the catalog in reach while
// the very top item stays a stretch. Lifestyle remains a real lever (frugal
// saves >2x extravagant). Tuned against the lifetime metrics sim.
export const SAVINGS_RATES: Record<SpendingHabit, number> = {
  frugal: 0.50, // 50% banked - conservative lifestyle
  normal: 0.35, // 35% banked - balanced lifestyle
  extravagant: 0.20, // 20% banked - high spending lifestyle
};

/**
 * Monthly expense modifiers by spending habit
 * Base expenses are multiplied by these values
 */
export const EXPENSE_MODIFIERS: Record<SpendingHabit, number> = {
  frugal: 0.7, // 30% less expenses due to frugal living
  normal: 1.0, // Standard expenses
  extravagant: 1.5, // 50% more expenses due to lavish lifestyle
};

/**
 * Base monthly expenses (rent, utilities, food, etc.)
 */
export const BASE_MONTHLY_EXPENSES = 800;

/**
 * Part-time job income modifier (30% of full salary)
 */
export const PART_TIME_MODIFIER = 0.3;
import { EventResult } from '../events/base.js';
import {
  isTutorialMode,
  checkTutorialTriggers,
} from '../events/tutorial/onboarding.js';
import { getFocus } from '../services/education/education_manager.js';

/**
 * Handle player reconnection and display offline time message
 */
export function connect(player: Player): void {
  player.connection = 'connected';
  const minutes = player.offlineStats?.minutesOffline ?? 0;

  if (minutes > 0) {
    let unit: string;
    let time: number;

    if (minutes < 60) {
      unit = 'minute';
      time = minutes;
    } else if (minutes < 1440) {
      unit = 'hour';
      time = Math.floor(minutes / 60);
    } else {
      unit = 'day';
      time = Math.floor(minutes / 1440);
    }

    if (time > 1) {
      unit += 's';
    }

    player.messageQueue = player.messageQueue ?? [];
    player.messageQueue.push(`Welcome back, you missed ${time} ${unit} of your life.`);

    if (player.offlineStats) {
      player.offlineStats.minutesOffline = 0;
    }
  }
}

/**
 * Legacy dilemma polling path removed from active runtime.
 * @deprecated Active runtime uses events/v2 engine in PlayerSession.
 */
export function checkDilemmas(_player: Player): EventResult {
  return null;
}

/**
 * Legacy event polling path removed from active runtime.
 * @deprecated Active runtime uses events/v2 engine in PlayerSession.
 */
export function checkEvents(_player: Player, _type: string = 'check'): EventResult {
  return null;
}

/**
 * Check and trigger tutorial events if player is in tutorial mode
 * @deprecated Active runtime uses events/v2 engine in PlayerSession.
 */
export function checkTutorialEvents(player: Player, type: string = 'check'): EventResult {
  try {
    // First check if player is in tutorial mode
    if (!isTutorialMode(player)) {
      return null;
    }

    // Get tutorial triggers for current state
    const triggers = checkTutorialTriggers(player);

    for (const triggerFunc of triggers) {
      try {
        const result = triggerFunc(player, type as 'message' | 'question' | 'answer');
        if (result) {
          // Mark the tip as delivered so it doesn't re-fire every tick. The
          // tip generators gate on `!askedQuestions.has(fname)` but
          // createMessageEvent never adds the key (only question events
          // self-marked), so message-type tips repeated forever on the offline
          // loop. Marking the fired event's id here fixes all of them at once.
          const firedId = (result as { id?: string }).id;
          if (firedId) player.askedQuestions.add(firedId);
          return result;
        }
      } catch (error) {
        console.error('Error in tutorial event:', error);
      }
    }
  } catch (error) {
    console.error('Error checking tutorial events:', error);
  }

  return null;
}

/**
 * Check if a schedule has been completed
 */
export function scheduleComplete(person: Person, scheduleId: string): boolean {
  if (!person.schedules) return false;

  for (const schedule of person.schedules) {
    if (
      schedule.id &&
      schedule.id.includes(scheduleId) &&
      schedule.executions === schedule.duration
    ) {
      return true;
    }
  }

  return false;
}

/**
 * Update character mood based on energy and happiness levels
 */
export function handleMoods(_player: Player, person: Person): void {
  const energy = person.energy ?? 50;
  const happiness = person.happiness ?? 50;

  if (energy < 40 && energy > 10) {
    person.mood = 'Stressed';
  }
  if (energy <= 10) {
    person.mood = 'Exhausted';
  }
  if (energy >= 40) {
    person.mood = 'Calm';
  }
  if (energy >= 40 && happiness >= 60) {
    person.mood = 'Fulfilled';
  }
  if (energy >= 40 && happiness <= 40) {
    person.mood = 'Depressed';
  }
  if (energy >= 40 && happiness > 40 && happiness < 60) {
    person.mood = 'Happy';
  }
}

// ============================================================================
// Finance Helper Functions
// ============================================================================

/**
 * Get the savings rate for a given spending habit
 *
 * @param spendingHabit - The person's spending habit type
 * @returns The savings rate as a decimal (0.05 to 0.20)
 */
export function getSavingsRate(spendingHabit: SpendingHabit | undefined): number {
  const habit = spendingHabit ?? 'normal';
  return SAVINGS_RATES[habit] ?? SAVINGS_RATES.normal;
}

/**
 * Get the expense modifier for a given spending habit
 *
 * @param spendingHabit - The person's spending habit type
 * @returns The expense modifier (0.7 to 1.5)
 */
export function getExpenseModifier(spendingHabit: SpendingHabit | undefined): number {
  const habit = spendingHabit ?? 'normal';
  return EXPENSE_MODIFIERS[habit] ?? EXPENSE_MODIFIERS.normal;
}

/**
 * Calculate monthly expenses based on spending habits
 *
 * Monthly expenses are calculated as:
 * - Base expenses ($800) multiplied by spending habit modifier
 * - frugal: $800 * 0.7 = $560/month
 * - normal: $800 * 1.0 = $800/month
 * - extravagant: $800 * 1.5 = $1200/month
 *
 * @param person - The person to calculate expenses for
 * @returns Monthly expense amount
 */
export function calculateMonthlyExpenses(person: Person): number {
  const habit = (person.spendingHabits as SpendingHabit) ?? 'normal';
  const modifier = getExpenseModifier(habit);
  return Math.round(BASE_MONTHLY_EXPENSES * modifier);
}

/**
 * Calculate weekly expenses based on spending habits
 *
 * @param person - The person to calculate expenses for
 * @returns Weekly expense amount (monthly / 4)
 */
export function calculateWeeklyExpenses(person: Person): number {
  return Math.round(calculateMonthlyExpenses(person) / 4);
}

/**
 * Receive weekly salary and apply savings based on spending habits
 *
 * This function processes a weekly salary payment:
 * 1. Applies part-time modifier if applicable (30% of full salary)
 * 2. Calculates savings based on spending habit (5-20%)
 * 3. Adds savings to person's money
 *
 * Savings rates by habit:
 * - frugal: 20% of income saved
 * - normal: 10% of income saved
 * - extravagant: 5% of income saved
 *
 * @param person - The person receiving salary
 * @param weeklySalary - The gross weekly salary amount
 * @returns Object containing savings amount and net income after savings
 */
export function receiveSalary(
  person: Person,
  weeklySalary: number
): { savings: number; netIncome: number } {
  if (weeklySalary <= 0) {
    return { savings: 0, netIncome: 0 };
  }

  // Apply part-time modifier if applicable
  let adjustedSalary = weeklySalary;
  const job = person.job as { hourType?: string } | undefined;
  if (job?.hourType === 'partTime') {
    adjustedSalary = weeklySalary * PART_TIME_MODIFIER;
  }

  // Calculate savings based on spending habits
  const savingsRate = getSavingsRate(person.spendingHabits as SpendingHabit);
  const savings = Math.round(adjustedSalary * savingsRate * 100) / 100;

  // Add savings to person's money
  person.money = (person.money ?? 0) + savings;

  // Net income is what's left after savings (spent on lifestyle)
  const netIncome = adjustedSalary - savings;

  return { savings, netIncome };
}

/**
 * Handle character finances including income and savings
 * Ported from Python stats_manager.py handleFinances()
 *
 * This function processes finances based on activity records:
 * 1. Iterates through activity records to find job income
 * 2. Applies part-time modifier if applicable (30% of full salary)
 * 3. Calculates savings based on spending habit (5-20%)
 * 4. Adds savings to person's money
 *
 * Spending habit effects on savings rate:
 * - frugal: 20% savings (saves more, spends less)
 * - normal: 10% savings (balanced lifestyle)
 * - extravagant: 5% savings (spends more, saves less)
 *
 * @param person - The person whose finances to handle
 */
export function handleFinances(person: Person): void {
  // Get savings rate based on spending habits
  const savingRatio = getSavingsRate(person.spendingHabits as SpendingHabit);

  // Calculate income from job activity records
  let income = 0;

  if (person.job) {
    for (const record of person.activityRecords ?? []) {
      if (record.type === 'job' && record.level?.salary) {
        income += record.level.salary;
      }
    }

    if (income > 0) {
      // Apply part-time modifier if applicable
      const job = person.job as { hourType?: string };
      if (job.hourType === 'partTime') {
        income *= PART_TIME_MODIFIER;
      }

      // Add savings to money (savings = income * savingsRate)
      const savings = Math.round(income * savingRatio * 100) / 100;
      person.money = (person.money ?? 0) + savings;
    }
  }
}

/**
 * Apply monthly expenses to a person's money
 * Should be called once per month (every 30 game days)
 *
 * @param person - The person to deduct expenses from
 * @returns The amount deducted
 */
export function applyMonthlyExpenses(person: Person): number {
  const expenses = calculateMonthlyExpenses(person);
  person.money = Math.max(0, (person.money ?? 0) - expenses);
  return expenses;
}

/**
 * Apply weekly expenses to a person's money
 * Should be called once per week (every 7 game days)
 *
 * @param person - The person to deduct expenses from
 * @returns The amount deducted
 */
export function applyWeeklyExpenses(person: Person): number {
  const expenses = calculateWeeklyExpenses(person);
  person.money = Math.max(0, (person.money ?? 0) - expenses);
  return expenses;
}

/**
 * Get a financial summary for a person
 * Useful for displaying financial information to the player
 *
 * @param person - The person to get financial info for
 * @returns Financial summary object
 */
export function getFinancialSummary(person: Person): {
  currentMoney: number;
  spendingHabit: SpendingHabit;
  savingsRate: number;
  monthlyExpenses: number;
  weeklyExpenses: number;
  savingsRatePercent: string;
} {
  const habit = (person.spendingHabits as SpendingHabit) ?? 'normal';
  const savingsRate = getSavingsRate(habit);

  return {
    currentMoney: person.money ?? 0,
    spendingHabit: habit,
    savingsRate,
    monthlyExpenses: calculateMonthlyExpenses(person),
    weeklyExpenses: calculateWeeklyExpenses(person),
    savingsRatePercent: `${Math.round(savingsRate * 100)}%`,
  };
}

/**
 * Calculate peak energy based on activities, habits, and focus settings
 * Updates person.peakEnergy and person.calcEnergy
 */
export function getPeakEnergy(person: Person): void {
  let peakEnergy = 0;

  // Quitting habits cost energy
  for (const habit of person.habits ?? []) {
    if (habit.status === 'quitting') {
      peakEnergy += 5;
    }
  }

  // Activities cost energy
  for (const activity of person.activities ?? []) {
    if (activity.energyModifier) {
      peakEnergy += activity.energyModifier;
    }

    // Check activity records for job energy and focus
    for (const record of person.activityRecords ?? []) {
      if (record.id === activity.id) {
        // JobLevel uses snake_case: energy_modifier
        if (record.type === 'job' && record.level?.energy_modifier) {
          peakEnergy += record.level.energy_modifier;
        }

        // Focus uses camelCase: energyModifier
        if (record.focus) {
          const focus = getFocus(record.focus);
          if (focus?.energyModifier) {
            peakEnergy += focus.energyModifier;
          }
        }
      }
    }
  }

  person.peakEnergy = peakEnergy;
  person.calcEnergy = (person.energy ?? 100) - peakEnergy;
}

/**
 * Update age for player and all relationships, with affinity decay
 * Ported from Python stats_manager.py updateAge()
 *
 * Decay mechanics:
 * - Player age increments hourly (ageHours) and daily (ageDays)
 * - Every 30 days (for player > 18 years): affinity -= 1 for all relationships
 * - On relationship birthday (yearly): affinity -= 1 + death chance update
 * - Affinity floor at -100
 *
 * @returns Event result for birthday messages, or null
 */
export function updateAge(player: Player): EventResult {
  if (!player.c) return null;

  // Increment player's age in hours
  player.c.ageHours = (player.c.ageHours ?? 0) + 1;

  // Daily tick check (every 24 hours)
  if ((player.c.ageHours ?? 0) % 24 === 0) {
    player.c.ageDays = (player.c.ageDays ?? 0) + 1;

    // Process age and decay for all relationships
    for (let i = 0; i < (player.r?.length ?? 0); i++) {
      const person = player.r[i];
      if (person.status !== 'alive') continue;

      // Increment relationship's age
      person.ageDays = (person.ageDays ?? 0) + 1;

      // Monthly affinity decay (every 30 days) for adult players
      if ((player.c.ageYears ?? 0) > 18 && (person.ageDays ?? 0) % 30 === 0) {
        person.affinity = (person.affinity ?? 0) - 1;
      }

      // Yearly birthday check for relationships
      if ((person.ageDays ?? 0) % 365 === 0) {
        person.ageYears = (person.ageYears ?? 0) + 1;
        person.affinity = (person.affinity ?? 0) - 1; // Annual affinity decay

        // Update death chance (imported from health_manager if available)
        const age = person.ageYears ?? 0;
        if (age < 30) person.deathChance = 0.00001;
        else if (age < 50) person.deathChance = 0.0001;
        else if (age < 70) person.deathChance = 0.001;
        else if (age < 80) person.deathChance = 0.01;
        else if (age < 90) person.deathChance = 0.05;
        else if (age < 100) person.deathChance = 0.15;
        else person.deathChance = 0.3;

        // Send birthday message for titled relationships (not classmates)
        const relationships = person.relationships ?? [];
        const isClassmate = relationships.includes('classmate');
        const title = (person as { title?: string }).title;

        if (!isClassmate && title) {
          // Dedup per person, per year. Key on the person's STABLE id (not
          // firstname, which is neither unique across NPCs nor immutable) so
          // two relatives who share a first name don't suppress each other.
          // The `player.events` guard makes this birthday idempotent: it fires
          // exactly once even if updateAge runs many times across the birthday
          // day, survives save/load (player.events round-trips via toJSON), and
          // is safe under the offline-iteration loop.
          const npcKey = `birthday_npc_${person.id || person.firstname}_${person.ageYears}`;
          if (!player.events.has(npcKey)) {
            player.events.add(npcKey);
            return {
              type: 'messageEvent',
              id: npcKey,
              message: `Your ${title} ${person.firstname} ${person.lastname} is now ${person.ageYears} years old.`,
            };
          }
        }
      }

      // Check for relationship death
      const deathChance = person.deathChance ?? 0;
      const health = (person as { health?: number }).health ?? 1;
      if (deathChance * health * 100 > Math.random() * 100 || (person.ageYears ?? 0) > 120) {
        person.status = 'dead';
        const title = (person as { title?: string }).title ?? 'friend';
        // Queue death message via messageQueue
        player.messageQueue = player.messageQueue ?? [];
        player.messageQueue.push(`Your ${title} ${person.firstname} ${person.lastname} has died at the age of ${person.ageYears} years old.`);
      }

      // Enforce affinity floor at -100
      if ((person.affinity ?? 0) < -100) {
        person.affinity = -100;
      }
    }

    // Check for player's birthday
    if ((player.c.ageDays ?? 0) > 0 && (player.c.ageDays ?? 0) % 365 === 0) {
      player.c.ageYears = (player.c.ageYears ?? 0) + 1;

      // Idempotent: emit the player's birthday exactly once per year. The
      // `player.events` guard prevents repeats across save/load and the
      // offline-iteration loop. Id format is kept as `birthday_<ageYears>`
      // (the client and lifecycle tests rely on it).
      const playerKey = `birthday_${player.c.ageYears}`;
      if (!player.events.has(playerKey)) {
        player.events.add(playerKey);
        return {
          type: 'messageEvent',
          id: playerKey,
          message: `${player.c.firstname} is now ${player.c.ageYears} years old!`,
        };
      }
    }
  }

  return null;
}

/**
 * Apply daily familiarity decay for all relationships
 * Ported from Python loop_manager.py daily tick
 *
 * Called at hour 0 (daily tick): familiarity -= 3 for alive relationships with familiarity > 0
 */
export function applyDailyFamiliarityDecay(player: Player): void {
  for (const person of player.r ?? []) {
    if (person.status === 'alive' && (person.familiarity ?? 0) > 0) {
      person.familiarity = (person.familiarity ?? 0) - 3;
      // Enforce floor at 0
      if ((person.familiarity ?? 0) < 0) {
        person.familiarity = 0;
      }
    }
  }
}

/**
 * Handle weekly relationship decay and random events
 * Ported from Python: handleRelationships in functions.py and relationship_manager.py
 *
 * Weekly decay mechanics:
 * - Familiarity decays by 1 per week (toward 0)
 * - Affinity decays by 1 per week for positive values (toward 0)
 * - 5% chance of random relationship events for romantic relationships
 */
export function handleRelationships(player: Player, _person: Person): void {
  // Import here to avoid circular dependency
  const { processWeeklyRelationshipEvents } = require('../events/relationships/index.js');

  for (const relPerson of player.r ?? []) {
    // Weekly familiarity decay
    if (relPerson.familiarity !== undefined && relPerson.familiarity > 0) {
      relPerson.familiarity = Math.max(0, relPerson.familiarity - 1);
    }

    // Weekly affinity decay (only positive affinity decays toward 0)
    if (relPerson.affinity !== undefined && relPerson.affinity > 0) {
      relPerson.affinity = Math.max(0, relPerson.affinity - 1);
    }
  }

  // Process 5% random relationship events for romantic relationships
  // Ported from Python handleRelationships() in relationship_manager.py
  const relationshipEvents = processWeeklyRelationshipEvents(player);

  // Queue any triggered events to message queue
  for (const event of relationshipEvents) {
    if (event && 'message' in event) {
      player.messageQueue = player.messageQueue ?? [];
      player.messageQueue.push(event.message);
    }
  }
}

/**
 * Set random likes and dislikes for a character
 */
export function setLikesDislikes(person: Person): Person {
  const possibleInterests = [
    'sports', 'music', 'art', 'reading', 'writing', 'science', 'math',
    'history', 'politics', 'philosophy', 'religion', 'nature', 'animals',
    'travel', 'food', 'cooking', 'cars', 'fashion', 'fitness', 'health',
    'technology', 'video games', 'movies', 'tv', 'theater', 'dance',
    'comedy', 'drama', 'horror', 'romance', 'action', 'adventure',
    'fantasy', 'sci-fi', 'mystery', 'thriller', 'crime', 'documentary',
    'anime', 'manga', 'cartoons', 'board games', 'card games',
    'Broccoli', 'Celery', 'Mushrooms', 'Brussels sprouts', 'Onions',
  ];

  // Pick random likes
  const likeCount = Math.floor(Math.random() * 5) + 1;
  const likes: string[] = [];
  const availableForLikes = [...possibleInterests];

  for (let i = 0; i < likeCount && availableForLikes.length > 0; i++) {
    const idx = Math.floor(Math.random() * availableForLikes.length);
    likes.push(availableForLikes[idx]);
    availableForLikes.splice(idx, 1);
  }

  // Pick random dislikes from remaining
  const dislikeCount = Math.floor(Math.random() * 5) + 1;
  const dislikes: string[] = [];

  for (let i = 0; i < dislikeCount && availableForLikes.length > 0; i++) {
    const idx = Math.floor(Math.random() * availableForLikes.length);
    dislikes.push(availableForLikes[idx]);
    availableForLikes.splice(idx, 1);
  }

  person.likes = likes;
  person.dislikes = dislikes;

  return person;
}

/**
 * Handle incremental state updates to send to client
 * Only includes changed values to minimize data transfer
 */
export function handleUpdates(
  updateObject: Record<string, unknown>,
  player: Player,
  _websocket: unknown
): Record<string, unknown> | false {
  const c = player.c as unknown as Record<string, unknown>;
  const attributes = [
    'energy', 'calcEnergy', 'happiness', 'stress', 'money', 'prestige',
    'occupation', 'intraDayMessage', 'location',
  ];

  // Check scalar attributes
  for (const attr of attributes) {
    const playerValue = c[attr];
    if (playerValue !== updateObject[attr]) {
      updateObject[attr] = playerValue;
    } else {
      delete updateObject[attr];
    }
  }

  // Check list attributes
  const listAttributes = ['schedules', 'dailyPlan'];
  for (const attr of listAttributes) {
    const playerValue = (c[attr] as unknown[]) ?? [];
    const updateValue = (updateObject[attr] as unknown[]) ?? [];
    if (playerValue.length !== updateValue.length) {
      updateObject[attr] = playerValue;
    } else {
      delete updateObject[attr];
    }
  }

  // Check game speed
  if (player.gameSpeed !== updateObject['gameSpeed']) {
    updateObject['gameSpeed'] = player.gameSpeed;
  } else {
    delete updateObject['gameSpeed'];
  }

  // Check time updates
  if (player.hourOfDay !== updateObject['hourOfDay']) {
    updateObject['date'] = player.date;
    updateObject['hourOfDay'] = player.hourOfDay;
    updateObject['weekDayText'] = player.weekDayText;
  } else if (
    player.gameSpeed <= 1 &&
    player.date !== updateObject['date'] &&
    player.hourOfDay === 0
  ) {
    updateObject['date'] = player.date;
    updateObject['hourOfDay'] = player.hourOfDay;
    updateObject['weekDayText'] = player.weekDayText;
  } else {
    delete updateObject['date'];
    delete updateObject['hourOfDay'];
    delete updateObject['weekDayText'];
  }

  // Return update object if it has content
  if (Object.keys(updateObject).length > 0) {
    updateObject['type'] = 'u';
    return updateObject;
  }

  return false;
}

/**
 * Parse and trigger one-time scheduled events
 */
export function parseOneTimeEvents(player: Player): void {
  const oneTimeEvents = player.c.oneTimeEvents ?? [];

  for (let i = oneTimeEvents.length - 1; i >= 0; i--) {
    const event = oneTimeEvents[i];
    if (event.date === player.date && player.hourOfDay === event.hour) {
      player.messageQueue = player.messageQueue ?? [];
      player.messageQueue.push(event.message);
      oneTimeEvents.splice(i, 1);

      // Run completion function if exists
      if (event.completionFunc && typeof event.runFunc === 'function') {
        event.runFunc(player);
      }
    }
  }
}

// Export all functions
export const statsManager = {
  // Connection
  connect,

  // Events
  checkDilemmas,
  checkEvents,
  checkTutorialEvents,
  parseOneTimeEvents,

  // Schedule
  scheduleComplete,

  // Moods
  handleMoods,

  // Finance functions
  getSavingsRate,
  getExpenseModifier,
  calculateMonthlyExpenses,
  calculateWeeklyExpenses,
  receiveSalary,
  handleFinances,
  applyMonthlyExpenses,
  applyWeeklyExpenses,
  getFinancialSummary,

  // Energy
  getPeakEnergy,

  // Relationships
  handleRelationships,
  applyDailyFamiliarityDecay,

  // Age
  updateAge,

  // Preferences
  setLikesDislikes,

  // Updates
  handleUpdates,
};
