/**
 * Intraday Activity System
 *
 * Generates and manages daily schedules for characters.
 * Handles different life stages: preschool, student, worker, retired.
 *
 * Ported from Python ws/intradayActivity.py
 */

import { Player, Person, GameLocation } from '../../models/index.js';
import { pickVariant } from '../../utils/textVariations.js';
import { applyMealEffect } from '../../services/health/health_manager.js';

/**
 * Plan-entry names that represent a meal slot (the ONLY hunger/thirst sink).
 * getDailyPlan tags breakfast/lunch/dinner entries with these names across every
 * life stage (preschool, student, worker, retired, holiday), so matching on the
 * name reliably identifies a meal regardless of the display title variant.
 */
const MEAL_PLAN_NAMES = new Set(['breakfast', 'lunch', 'dinner']);

// ── Text variant arrays for high-frequency daily messages ──

const wakeUpVariants = [
  'You wake up',
  'You open your eyes and start the day',
  'You roll out of bed',
];

const wakeUpSchoolVariants = [
  'You wake up for school',
  'Your alarm goes off — time for school',
  'You drag yourself out of bed for school',
];

const arriveWorkVariants = [
  'You arrive at work',
  'You clock in at work',
  'You settle in at your desk',
];

const finishWorkVariants = [
  'You finish work',
  'You wrap up for the day',
  'You clock out',
];

const arriveHomeVariants = [
  'You arrive home',
  'You get back home',
  'You walk through the front door',
];

const goToBedVariants = [
  'You go to bed',
  'You turn off the lights and go to sleep',
  'You call it a night',
];

const goToSleepVariants = [
  'You go to sleep',
  'You drift off to sleep',
  'You finally fall asleep',
];

const eatBreakfastVariants = [
  'You eat breakfast',
  'You grab a quick breakfast',
  'You sit down for breakfast',
];

const eatLunchVariants = [
  'You eat lunch',
  'You take a lunch break',
  'You grab something to eat',
];

const eatDinnerVariants = [
  'You eat dinner',
  'You sit down for dinner',
  'You have dinner',
];

export interface DailyEvent {
  time: number;
  location: string;
  title: string;
  name: string;
}

/**
 * Player-initiated activity definition.
 *
 * These give the player proactive agency: instead of only reacting to event
 * prompts, the player can choose to spend a free daily-plan slot on a concrete
 * action with a known tradeoff. The numeric effects are the single source of
 * truth shared between the `performActivity` command handler and the daily-plan
 * override path, so the evening/weekend auto-roll and the player's chosen
 * activity apply identical deltas.
 */
export interface PlayerActivity {
  /** Stable id the client sends in the performActivity payload. */
  id: string;
  /** Plan-entry name used by getIntradayActivity (matches getRandomEveningActivity names). */
  name: string;
  /** Display title shown on the daily plan / intraday message. */
  title: string;
  /** Energy required AND consumed to perform the activity (cannot perform below this). */
  energyCost: number;
  /** Optional minimum age (player must be at least this old). */
  minAge?: number;
  /** Stat/resource deltas applied when the activity is performed. */
  effects: {
    intelligence?: number;
    social?: number;
    creativity?: number;
    happiness?: number;
    health?: number;
    stress?: number;
    money?: number;
    affinity?: number;
  };
}

/**
 * The set of player-initiated activities exposed via the performActivity command.
 * Effect magnitudes are intentionally modest and mirror the kinds of evening
 * activities the auto-scheduler rolls (study/exercise/socialize/hobby), so a
 * player choosing an activity gets the same scale of reward the random roll
 * would have produced — just under their control and with an energy cost.
 */
export const PLAYER_ACTIVITIES: Record<string, PlayerActivity> = {
  study: {
    id: 'study',
    name: 'study',
    title: 'You study to sharpen your mind',
    energyCost: 10,
    effects: { intelligence: 3, stress: 2, happiness: -1 },
  },
  exercise: {
    id: 'exercise',
    name: 'exercise',
    title: 'You do some exercise',
    energyCost: 15,
    effects: { health: 3, stress: -3, happiness: 1 },
  },
  socialize: {
    id: 'socialize',
    name: 'social',
    title: 'You call a friend to catch up',
    energyCost: 8,
    // NOTE: no `affinity` here. Affinity is an NPC->player closeness scalar; the
    // old `affinity: 2` was applied to the player's own person.affinity, which is
    // meaningless (never read). The increase_affinity daily quest is fired at the
    // handler level for the socialize path instead.
    effects: { social: 3, happiness: 2, stress: -2 },
  },
  sideHustle: {
    id: 'sideHustle',
    name: 'side-hustle',
    title: 'You work on a side hustle for extra cash',
    energyCost: 18,
    minAge: 14,
    effects: { money: 40, stress: 3, happiness: 1 },
  },
  hobby: {
    id: 'hobby',
    name: 'hobby',
    title: 'You work on a hobby',
    energyCost: 6,
    effects: { creativity: 3, happiness: 2, stress: -2 },
  },
};

/**
 * Resolve a player activity by its id. Returns undefined for unknown ids.
 */
export function getPlayerActivity(activityId: string): PlayerActivity | undefined {
  return PLAYER_ACTIVITIES[activityId];
}

// ── Skill progression + varied-outcome model (T006: deepen performActivity) ──
//
// The proactive agency loop used to apply flat fixed deltas forever (study =
// +3 intelligence every time). It now builds a per-activity SKILL (persisted on
// person.skills, keyed by activity id) and scales each performance's stat gains
// along a real progression curve plus a varied per-performance outcome roll.

/** Outcome tier of a single activity performance. */
export type ActivityOutcomeTier = 'poor' | 'normal' | 'great';

/**
 * Skill-tier mastery multiplier (FRONT-LOADED diminishing returns).
 *
 * Early levels give the biggest jumps so the first sessions feel rewarding,
 * then the multiplier tapers toward the base rate as the skill matures. Net
 * effect: repeated practice traces a real curve (steep early, flattening) that
 * KEEPS climbing because the skill itself keeps growing — never a flat +3.
 *
 *   novice      (skill   0-4 ) -> 1.6x
 *   apprentice  (skill   5-14) -> 1.3x
 *   skilled     (skill  15-29) -> 1.15x
 *   expert      (skill  30+  ) -> 1.0x
 */
export function skillMasteryMultiplier(skillLevel: number): number {
  if (skillLevel >= 30) return 1.0;
  if (skillLevel >= 15) return 1.15;
  if (skillLevel >= 5) return 1.3;
  return 1.6;
}

/** Human-readable mastery label for the given skill level. */
export function skillTierLabel(skillLevel: number): string {
  if (skillLevel >= 30) return 'expert';
  if (skillLevel >= 15) return 'skilled';
  if (skillLevel >= 5) return 'apprentice';
  return 'novice';
}

/**
 * Outcome-variance multiplier and extra skill gain for a tier.
 *  - poor:   0.5x magnitude, no bonus skill
 *  - normal: 1.0x magnitude, no bonus skill
 *  - great:  1.75x magnitude, +1 bonus skill (a breakthrough session)
 */
export function outcomeTierMultiplier(tier: ActivityOutcomeTier): number {
  switch (tier) {
    case 'poor':
      return 0.5;
    case 'great':
      return 1.75;
    case 'normal':
    default:
      return 1.0;
  }
}

/**
 * Roll a varied outcome tier from an injectable RNG (defaults to Math.random so
 * production stays random; tests pass a seeded generator for determinism).
 *   roll < 0.15            -> poor
 *   0.15 <= roll < 0.85    -> normal
 *   roll >= 0.85           -> great
 */
export function rollOutcomeTier(rng: () => number = Math.random): ActivityOutcomeTier {
  let roll = rng();
  if (!Number.isFinite(roll) || roll < 0) roll = 0;
  if (roll >= 1) roll = 1 - Number.EPSILON;
  if (roll < 0.15) return 'poor';
  if (roll < 0.85) return 'normal';
  return 'great';
}

/** Short, tier-flavored feedback line for the activityPerformed payload. */
export function activityOutcomeMessage(
  activity: PlayerActivity,
  tier: ActivityOutcomeTier
): string {
  switch (tier) {
    case 'poor':
      return `${activity.title} — but it didn't go very well.`;
    case 'great':
      return `${activity.title} — a breakthrough session!`;
    case 'normal':
    default:
      return activity.title;
  }
}

/**
 * Compute the actual stat/resource deltas for one performance of an activity,
 * scaling the activity's base effects by BOTH the current skill mastery
 * multiplier and the rolled outcome-tier multiplier, and reporting the next
 * skill level (incl. the +1 base increment and any tier breakthrough bonus).
 *
 * Magnitudes are scaled then rounded so they stay integers, with a guarantee
 * that any non-zero base effect still moves the stat by at least 1 in its
 * original direction (so a 0.5x "poor" study session still teaches *something*).
 * This is the SINGLE source of truth for the deltas — the handler applies them.
 */
export function resolveActivityOutcome(
  activity: PlayerActivity,
  skillLevel: number,
  tier: ActivityOutcomeTier
): { deltas: PlayerActivity['effects']; nextSkillLevel: number } {
  const multiplier = skillMasteryMultiplier(skillLevel) * outcomeTierMultiplier(tier);

  const scale = (base: number | undefined): number | undefined => {
    if (!base) return base; // undefined / 0 stay as-is
    const scaled = base * multiplier;
    const rounded = Math.round(scaled);
    // Preserve direction: never let a non-zero base round to 0.
    if (rounded === 0) return base > 0 ? 1 : -1;
    return rounded;
  };

  const e = activity.effects;
  const deltas: PlayerActivity['effects'] = {
    intelligence: scale(e.intelligence),
    social: scale(e.social),
    creativity: scale(e.creativity),
    happiness: scale(e.happiness),
    health: scale(e.health),
    stress: scale(e.stress),
    money: scale(e.money),
    affinity: scale(e.affinity),
  };

  // Skill always grows by 1 per performance; a "great" breakthrough adds +1.
  const bonus = tier === 'great' ? 1 : 0;
  const nextSkillLevel = skillLevel + 1 + bonus;

  return { deltas, nextSkillLevel };
}

/**
 * Whether a person is too exhausted to perform an energy-costing activity.
 *
 * Gate on RAW energy: an activity is blocked only when the character cannot
 * afford its energy cost out of their raw `energy` pool. A zero-cost activity is
 * always allowed. This MUST match the server handler's energy gate
 * (performActivity.ts: `energy < energyCost`) and the iOS client gate, otherwise
 * the client shows "Low energy" for activities the server would happily accept.
 *
 * Note: we deliberately do NOT gate on `calcEnergy` (raw energy minus the
 * permanent peakEnergy reserve). A high-school student carries peakEnergy ~20
 * permanently, so calcEnergy hovers near 0 and a calcEnergy gate falsely blocks
 * every activity. The calcEnergy-exhaustion PENALTY in applyHourlySurvival stays
 * — scarcity is still a consequence, it just no longer falsely locks activities.
 */
export function isTooExhaustedForActivity(
  person: Person,
  activity: Pick<PlayerActivity, 'energyCost'>
): boolean {
  const cost = activity.energyCost ?? 0;
  if (cost <= 0) return false;
  return (person.energy ?? 0) < cost;
}

export interface Schedule {
  id: string;
  title: string;
  location: string;
  duration: number;
  executions: number;
  days: {
    daysOfWeek: string[];
    hour: number;
  };
}

export interface Location {
  id: string;
  type: string;
  name?: string;
}

/**
 * Create a daily event
 */
function createDailyEvent(time: number, location: string): DailyEvent {
  return {
    time,
    location,
    title: '',
    name: '',
  };
}

/**
 * Find a location by ID
 */
function findLocationById(locations: GameLocation[], id: string): GameLocation | undefined {
  return locations.find(l => l.id === id);
}

/**
 * Create a new location
 */
function createLocation(id: string, type: string): GameLocation {
  return { id, type };
}

/**
 * Get random integer in range (inclusive)
 */
function randomInt(min: number, max: number): number {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

/**
 * Get random element from array
 */
function randomChoice<T>(arr: T[]): T | undefined {
  if (arr.length === 0) return undefined;
  return arr[Math.floor(Math.random() * arr.length)];
}

/**
 * Get activities of a specific type (like Python getFromArray)
 */
function getActivitiesByType(activities: unknown[], type: string): unknown[] {
  if (!Array.isArray(activities)) return [];
  return activities.filter((a: unknown) => {
    if (typeof a === 'object' && a !== null && 'type' in a) {
      return (a as { type: string }).type === type;
    }
    return false;
  });
}

/**
 * Check if it's a weekend based on day of week
 * Python uses string-based comparison, TypeScript uses numbers
 * dayOfWeek: 1=Sunday, 2=Monday, ..., 6=Friday, 7=Saturday
 */
function isWeekendDay(dayOfWeek: number): boolean {
  // Saturday = 7, Sunday = 1
  return dayOfWeek === 1 || dayOfWeek === 7;
}

/**
 * Get day name from day of week number
 * dayOfWeek: 1=Sunday, 2=Monday, ..., 6=Friday, 7=Saturday
 */
function getDayName(dayOfWeek: number): string {
  const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
  return days[(dayOfWeek - 1) % 7] || 'Monday';
}

/**
 * Get intraday activity for current time
 * Updates person's location and intraDayMessage based on their daily plan
 */
export function getIntradayActivity(player: Player, person: Person): Person {
  // Store previous message for comparison
  person.lastIntraDayMessage = person.intraDayMessage;
  person.intraDayMessage = '';

  const plan = person.dailyPlan ?? [];
  const currentTime = player.hourOfDay ?? 0;

  // Find event matching current time
  for (const event of plan) {
    if (typeof event === 'object' && event !== null && 'time' in event) {
      const e = event as DailyEvent;
      if (e.time === currentTime) {
        // Update location if it changed
        if (e.location !== person.location) {
          person.location = e.location;
        }
        // Set intraday message if event has a title
        if (e.title) {
          person.intraDayMessage = e.title;
        }
        // Meal slots are the ONLY hunger/thirst sink. This branch is the shared
        // executor for BOTH loops (PlayerSession online + LoopManager/GameEngine
        // offline both call getIntradayActivity), so feeding here keeps the two
        // paths from diverging.
        if (MEAL_PLAN_NAMES.has(e.name)) {
          applyMealEffect(person);
        }
        break;
      }
    }
  }

  return person;
}

/**
 * Generate daily plan for a person
 * Main entry point that dispatches to specific plan generators based on occupation
 */
export function getDailyPlan(player: Player, person: Person): Person {
  person.dailyPlan = [];
  const locations = (player.l ?? []) as GameLocation[];

  // Determine home location ID
  // If person is part of player's family (has 'self' relationship or familyLevel == 1),
  // use the player character's home
  let homeId = `home-${person.id}`;
  if (person.relationships?.includes('self') || person.familyLevel === 1) {
    homeId = `home-${player.c?.id ?? person.id}`;
  }

  // Get or create home location
  let homeObj = findLocationById(locations, homeId);
  if (!homeObj) {
    homeObj = createLocation(homeId, 'home');
    locations.push(homeObj);
  }
  const home = homeObj.id;

  // Get or create school location (always based on player character)
  const schoolId = `school-${player.c?.id ?? person.id}`;
  let schoolObj = findLocationById(locations, schoolId);
  if (!schoolObj) {
    schoolObj = createLocation(schoolId, 'school');
    locations.push(schoolObj);
  }
  const school = schoolObj.id;

  // Get or create work location (always based on player character)
  const workId = `work-${player.c?.id ?? person.id}`;
  let workObj = findLocationById(locations, workId);
  if (!workObj) {
    workObj = createLocation(workId, 'work');
    locations.push(workObj);
  }
  const work = workObj.id;

  // Update locations array on player
  player.l = locations;

  // Check weekend using day of week number
  const isWeekend = isWeekendDay(player.dayOfWeek ?? 1);
  player.weekend = isWeekend;

  // Process scheduled activities (custom schedules with duration tracking)
  const schedules = (person.schedules ?? []) as Schedule[];
  const dayName = getDayName(player.dayOfWeek ?? 1);

  for (const schedule of schedules) {
    if (
      schedule.duration &&
      schedule.days &&
      schedule.executions < schedule.duration &&
      schedule.days.daysOfWeek?.includes(dayName)
    ) {
      const e = createDailyEvent(schedule.days.hour, schedule.location);
      e.title = schedule.title;
      e.name = schedule.id;
      schedule.executions += 1;
      person.dailyPlan.push(e);

      // Notify player when a scheduled activity is completed
      if (schedule.executions === schedule.duration) {
        player.messageQueue = player.messageQueue ?? [];
        player.messageQueue.push(`"${schedule.title}" has been completed!`);
      }
    }
  }

  // Holiday events - return early if holiday plan is generated
  if (player.dayEvent) {
    generateHolidayPlan(player, person, home);
    return person;
  }

  // Retired events
  if ((person.ageYears ?? 0) > 50 && person.occupation === 'retired') {
    generateRetiredPlan(person, home);
    return person;
  }

  // Working adult events (adults 18+ who are not student, preschool, or retired)
  const age = person.ageYears ?? 0;
  if (
    age >= 18 &&
    person.occupation !== 'student' &&
    person.occupation !== 'preschool' &&
    person.occupation !== 'retired'
  ) {
    generateWorkerPlan(person, home, work, isWeekend);
    return person;
  }

  // Infant/preschool events (under 5 or explicitly preschool)
  if (person.occupation === 'preschool' || (age < 5 && age > 0)) {
    generatePreschoolPlan(person, home);
    return person;
  }

  // Student events (explicit student occupation OR school-age 5-17)
  if (person.occupation === 'student' || (age >= 5 && age < 18)) {
    // Weekend or summer vacation
    if (isWeekend || player.summerVacation) {
      generateStudentWeekendPlan(player, person, home, locations);
    } else {
      // Regular school day
      generateStudentSchoolDayPlan(person, home, school);
    }
    return person;
  }

  return person;
}

/**
 * Generate plan for holidays (Christmas, etc.)
 */
function generateHolidayPlan(
  player: Player,
  person: Person,
  home: string
): void {
  const wakeUpTime = 7;

  if (player.dayEvent === 'christmas') {
    const christmasWakeVariants = [
      'You wake up on Christmas morning',
      'You jump out of bed — it\'s Christmas!',
      'You wake up to the smell of Christmas breakfast',
    ];
    const wakeUp = createDailyEvent(wakeUpTime, home);
    wakeUp.title = pickVariant(christmasWakeVariants);
    wakeUp.name = 'wakeUp';
    person.dailyPlan!.push(wakeUp);

    const presents = createDailyEvent(wakeUpTime + 1, home);
    presents.title = 'You open gifts!';
    presents.name = 'christmas';
    person.dailyPlan!.push(presents);
    // Gift quality could be based on parent affinity (future enhancement)
  }

  // Add other holiday handlers here as needed
  // thanksgiving, easter, halloween, etc.
}

/**
 * Generate plan for retired person
 */
function generateRetiredPlan(person: Person, home: string): void {
  const wakeUpTime = randomInt(7, 9);

  const wakeUp = createDailyEvent(wakeUpTime, home);
  wakeUp.title = pickVariant(wakeUpVariants);
  wakeUp.name = 'wakeUp';
  person.dailyPlan!.push(wakeUp);

  const breakfast = createDailyEvent(wakeUpTime + 1, home);
  breakfast.title = pickVariant(eatBreakfastVariants);
  breakfast.name = 'breakfast';
  person.dailyPlan!.push(breakfast);

  const lunch = createDailyEvent(12, home);
  lunch.title = pickVariant(eatLunchVariants);
  lunch.name = 'lunch';
  person.dailyPlan!.push(lunch);

  const dinner = createDailyEvent(18, home);
  dinner.title = pickVariant(eatDinnerVariants);
  dinner.name = 'dinner';
  person.dailyPlan!.push(dinner);
}

/**
 * Get a random evening activity (18:00-22:00 time slot)
 * Provides variety instead of always showing "relax at home".
 *
 * Player agency: if `person` has a `plannedActivity` override set (via the
 * performActivity command targeting the upcoming evening slot), that activity
 * is honored instead of the random roll. The override is consumed (cleared)
 * once read so it only applies to the next evening rather than every day.
 */
function getRandomEveningActivity(person?: Person): { title: string; name: string } {
  if (person?.plannedActivity) {
    const planned = getPlayerActivity(person.plannedActivity);
    if (planned) {
      person.plannedActivity = undefined;
      return { title: planned.title, name: planned.name };
    }
    // Unknown id stored — drop it so we don't loop on a bad override.
    person.plannedActivity = undefined;
  }
  const activities = [
    { title: 'You cook dinner and clean up', name: 'cooking' },
    { title: 'You go for an evening walk', name: 'walk' },
    { title: 'You call a friend to catch up', name: 'social' },
    { title: 'You read a book', name: 'reading' },
    { title: 'You watch TV', name: 'entertainment' },
    { title: 'You do some exercise', name: 'exercise' },
    { title: 'You work on a hobby', name: 'hobby' },
    { title: 'You relax at home', name: 'home' },
    { title: 'You browse the internet', name: 'leisure' },
    { title: 'You play video games', name: 'gaming' },
    { title: 'You listen to music', name: 'music' },
    { title: 'You tidy up the house', name: 'chores' },
  ];
  return activities[randomInt(0, activities.length - 1)];
}

/**
 * Generate plan for working adult
 */
function generateWorkerPlan(
  person: Person,
  home: string,
  work: string,
  isWeekend: boolean
): void {
  const wakeUpTime = randomInt(5, 9);

  const wakeUp = createDailyEvent(wakeUpTime, home);
  wakeUp.title = pickVariant(wakeUpVariants);
  wakeUp.name = 'wakeUp';
  person.dailyPlan!.push(wakeUp);

  if (!isWeekend) {
    // Weekday - go to work
    const arriveWork = createDailyEvent(wakeUpTime + 1, work);
    arriveWork.title = pickVariant(arriveWorkVariants);
    arriveWork.name = 'work';
    person.dailyPlan!.push(arriveWork);

    const lunch = createDailyEvent(12, home); // Python has location = home but then doesn't use it
    lunch.title = pickVariant(eatLunchVariants);
    lunch.name = 'lunch';
    person.dailyPlan!.push(lunch);

    const finishWork = createDailyEvent(17, home);
    finishWork.title = pickVariant(finishWorkVariants);
    finishWork.name = 'work';
    person.dailyPlan!.push(finishWork);

    const arriveHome = createDailyEvent(18, home);
    arriveHome.title = pickVariant(arriveHomeVariants);
    arriveHome.name = 'home';
    person.dailyPlan!.push(arriveHome);

    const dinner = createDailyEvent(19, home);
    dinner.title = pickVariant(eatDinnerVariants);
    dinner.name = 'dinner';
    person.dailyPlan!.push(dinner);

    // Evening activity (varied instead of always "relax at home")
    const eveningActivity = getRandomEveningActivity(person);
    const relax = createDailyEvent(20, home);
    relax.title = eveningActivity.title;
    relax.name = eveningActivity.name;
    person.dailyPlan!.push(relax);

    // Bed time calculated relative to wake up time (8 hours before)
    let bedTime = wakeUpTime - 8;
    if (bedTime < 0) bedTime += 24;
    const bed = createDailyEvent(bedTime, home);
    bed.title = pickVariant(goToBedVariants);
    bed.name = 'bed';
    person.dailyPlan!.push(bed);
  } else {
    // Weekend for workers - varied activities instead of just "relax at home"
    const breakfast = createDailyEvent(wakeUpTime + 1, home);
    breakfast.title = pickVariant(eatBreakfastVariants);
    breakfast.name = 'breakfast';
    person.dailyPlan!.push(breakfast);

    // Weekend morning/midday activity (varied)
    const weekendActivities = [
      { title: 'You go for a walk in the park', name: 'park' },
      { title: 'You visit family', name: 'family' },
      { title: 'You do a deep clean of your home', name: 'chores' },
      { title: 'You go grocery shopping', name: 'shopping' },
      { title: 'You work on a personal project', name: 'hobby' },
      { title: 'You catch up on errands', name: 'errands' },
      { title: 'You sleep in and take it easy', name: 'rest' },
    ];
    const morningActivity = weekendActivities[randomInt(0, weekendActivities.length - 1)];
    const morning = createDailyEvent(wakeUpTime + 2, home);
    morning.title = morningActivity.title;
    morning.name = morningActivity.name;
    person.dailyPlan!.push(morning);

    const lunch = createDailyEvent(12, home);
    lunch.title = pickVariant(eatLunchVariants);
    lunch.name = 'lunch';
    person.dailyPlan!.push(lunch);

    // Afternoon activity
    const afternoonActivities = [
      { title: 'You hang out with friends', name: 'social' },
      { title: 'You watch a movie', name: 'entertainment' },
      { title: 'You go shopping', name: 'shopping' },
      { title: 'You exercise at the gym', name: 'exercise' },
      { title: 'You read a book', name: 'leisure' },
      { title: 'You take a nap', name: 'rest' },
    ];
    const afternoonActivity = afternoonActivities[randomInt(0, afternoonActivities.length - 1)];
    const afternoon = createDailyEvent(14, home);
    afternoon.title = afternoonActivity.title;
    afternoon.name = afternoonActivity.name;
    person.dailyPlan!.push(afternoon);

    const dinner = createDailyEvent(18, home);
    dinner.title = pickVariant(eatDinnerVariants);
    dinner.name = 'dinner';
    person.dailyPlan!.push(dinner);

    // Evening activity (varied instead of "relax at home")
    const eveningActivity = getRandomEveningActivity(person);
    const evening = createDailyEvent(20, home);
    evening.title = eveningActivity.title;
    evening.name = eveningActivity.name;
    person.dailyPlan!.push(evening);

    // Bed time calculated relative to wake up time (8 hours before)
    let bedTime = wakeUpTime - 8;
    if (bedTime < 0) bedTime += 24;
    const bed = createDailyEvent(bedTime, home);
    bed.title = pickVariant(goToBedVariants);
    bed.name = 'bed';
    person.dailyPlan!.push(bed);
  }
}

/**
 * Generate plan for preschooler/infant
 */
function generatePreschoolPlan(person: Person, home: string): void {
  const wakeUpTime = randomInt(7, 10);

  const wakeUp = createDailyEvent(wakeUpTime, home);
  wakeUp.title = pickVariant(wakeUpVariants);
  wakeUp.name = 'wakeUp';
  person.dailyPlan!.push(wakeUp);

  const breakfast = createDailyEvent(wakeUpTime + 1, home);
  breakfast.title = 'You are fed breakfast';
  breakfast.name = 'breakfast';
  person.dailyPlan!.push(breakfast);

  const lunch = createDailyEvent(12, home);
  lunch.title = 'You are fed lunch';
  lunch.name = 'lunch'; // Python has 'breakfast' here but that seems like a bug
  person.dailyPlan!.push(lunch);

  const dinner = createDailyEvent(18, home);
  dinner.title = 'You are fed dinner';
  dinner.name = 'dinner';
  person.dailyPlan!.push(dinner);

  const bed = createDailyEvent(20, home);
  bed.title = pickVariant(goToBedVariants);
  bed.name = 'bed';
  person.dailyPlan!.push(bed);
}

/**
 * Generate weekend/summer vacation plan for student
 */
function generateStudentWeekendPlan(
  player: Player,
  person: Person,
  home: string,
  locations: GameLocation[]
): void {
  const wakeUpTime = randomInt(7, 10);

  const wakeUp = createDailyEvent(wakeUpTime, home);
  wakeUp.title = pickVariant(wakeUpVariants);
  wakeUp.name = 'wakeUp';
  person.dailyPlan!.push(wakeUp);

  const breakfast = createDailyEvent(wakeUpTime + 1, home);
  breakfast.title = pickVariant(eatBreakfastVariants);
  breakfast.name = 'breakfast';
  person.dailyPlan!.push(breakfast);

  // Varied weekend morning activity for students
  const studentWeekendMorning = [
    { title: 'You play outside', name: 'play' },
    { title: 'You hang out with friends', name: 'social' },
    { title: 'You play video games', name: 'gaming' },
    { title: 'You watch cartoons', name: 'entertainment' },
    { title: 'You relax at home', name: 'weekend' },
    { title: 'You work on a hobby', name: 'hobby' },
    { title: 'You help with chores', name: 'chores' },
  ];
  const morningPick = studentWeekendMorning[randomInt(0, studentWeekendMorning.length - 1)];
  const relaxMorning = createDailyEvent(wakeUpTime + 2, home);
  relaxMorning.title = morningPick.title;
  relaxMorning.name = morningPick.name;
  person.dailyPlan!.push(relaxMorning);

  const lunch = createDailyEvent(12, home);
  lunch.title = pickVariant(eatLunchVariants);
  lunch.name = 'lunch'; // Python has 'breakfast' here but that seems like a bug
  person.dailyPlan!.push(lunch);

  // Saturday extracurriculars (sports games)
  const dayName = getDayName(player.dayOfWeek ?? 1);
  if (dayName === 'Saturday') {
    const activities = person.activities ?? [];
    const extracurriculars = getActivitiesByType(activities, 'extracurricular');

    if (extracurriculars.length > 0) {
      const activity = randomChoice(extracurriculars) as { title?: string } | undefined;
      if (activity?.title) {
        const sports = ['Football', 'Baseball', 'Soccer'];
        if (sports.includes(activity.title)) {
          const game = createDailyEvent(10, home);
          game.title = `You play a ${activity.title} game`;
          game.name = 'extracurricular';
          person.dailyPlan!.push(game);

          const result = createDailyEvent(11, home);
          result.title = randomInt(0, 1) === 0 ? 'You Win!' : 'You Lose!';
          result.name = 'extracurricular';
          person.dailyPlan!.push(result);
        }
      }
    }
  }

  const dinner = createDailyEvent(18, home);
  dinner.title = pickVariant(eatDinnerVariants);
  dinner.name = 'dinner';
  person.dailyPlan!.push(dinner);

  // Evening activities - multiple random paths like Python.
  // Player agency: if an evening override is queued, honor it directly and skip
  // the random homework/party branching so the chosen activity always lands.
  if (person.plannedActivity && getPlayerActivity(person.plannedActivity)) {
    const eveningActivity = getRandomEveningActivity(person);
    const eveningOverride = createDailyEvent(20, home);
    eveningOverride.title = eveningActivity.title;
    eveningOverride.name = eveningActivity.name;
    person.dailyPlan!.push(eveningOverride);
  } else if (randomInt(0, 1) === 1) {
    if (randomInt(0, 1) === 1) {
      const study = createDailyEvent(20, home);
      study.title = randomInt(0, 1) === 1 ? 'You do your homework' : 'You study';
      study.name = 'study';
      person.dailyPlan!.push(study);
    }
  } else if (randomInt(0, 1) === 1) {
    // Party option
    const partyId = `party-${player.c?.id ?? person.id}`;
    let partyLocation = findLocationById(locations, partyId);
    if (!partyLocation) {
      partyLocation = createLocation(partyId, 'party');
      locations.push(partyLocation);
      player.l = locations;
    }

    const party = createDailyEvent(20, partyLocation.id);
    party.title = 'You go to a party with friends';
    party.name = 'party';
    person.dailyPlan!.push(party);

    // Small chance of getting drunk (1 in 100)
    if (randomInt(0, 100) === 1) {
      const drunkEvent = createDailyEvent(22, partyLocation.id);
      drunkEvent.title = 'You get drunk and pass out';
      drunkEvent.name = 'party';
      person.dailyPlan!.push(drunkEvent);
    } else {
      const returnHome = createDailyEvent(22, home);
      returnHome.title = 'You return home safely';
      returnHome.name = 'party';
      person.dailyPlan!.push(returnHome);
    }
  } else {
    // Default: varied evening activity
    const eveningActivity = getRandomEveningActivity(person);
    const eveningRelax = createDailyEvent(20, home);
    eveningRelax.title = eveningActivity.title;
    eveningRelax.name = eveningActivity.name;
    person.dailyPlan!.push(eveningRelax);
  }

  const bedTime = randomInt(23, 24);
  const bed = createDailyEvent(bedTime, home);
  bed.title = pickVariant(goToSleepVariants);
  bed.name = 'sleep';
  person.dailyPlan!.push(bed);
}

/**
 * Generate school day plan for student
 * Matches Python timing exactly
 */
function generateStudentSchoolDayPlan(
  person: Person,
  home: string,
  school: string
): void {
  let currentTime = 7;

  // Wake up at 7
  const wakeUp = createDailyEvent(currentTime, home);
  wakeUp.title = pickVariant(wakeUpSchoolVariants);
  wakeUp.name = 'wakeUp';
  person.dailyPlan!.push(wakeUp);

  // On the way to school at 8
  currentTime = 8;
  const toSchool = createDailyEvent(currentTime, home);
  toSchool.title = "You're on the way to school";
  toSchool.name = 'school';
  person.dailyPlan!.push(toSchool);

  // School starts at 9 (currentTime + 1 after incrementing)
  currentTime = 9;
  const schoolStart = createDailyEvent(currentTime, school);
  schoolStart.title = 'Schoolday starts';
  schoolStart.name = 'school';
  person.dailyPlan!.push(schoolStart);

  // Second class at 10 (fixed from Python bug where it was at 8)
  currentTime = 10;
  const class2 = createDailyEvent(currentTime, school);
  class2.title = 'Your second class starts';
  class2.name = 'school';
  person.dailyPlan!.push(class2);

  // Schedule: 9=1st, 10=2nd, 11=3rd, 12=Lunch, 13=4th, 14=5th/Last
  const class3 = createDailyEvent(11, school);
  class3.title = 'Your third class starts';
  class3.name = 'school';
  person.dailyPlan!.push(class3);

  const lunch = createDailyEvent(12, school);
  lunch.title = 'Lunchtime';
  lunch.name = 'lunch';
  person.dailyPlan!.push(lunch);

  const class4 = createDailyEvent(13, school);
  class4.title = 'Your fourth class starts';
  class4.name = 'school';
  person.dailyPlan!.push(class4);

  const lastClass = createDailyEvent(14, school);
  lastClass.title = 'Your last class starts';
  lastClass.name = 'school';
  person.dailyPlan!.push(lastClass);

  // Extracurriculars after school
  const activities = person.activities ?? [];
  const extracurriculars = getActivitiesByType(activities, 'extracurricular');

  currentTime = 15;
  if (extracurriculars.length > 0) {
    const activity = randomChoice(extracurriculars) as { title?: string } | undefined;
    if (activity?.title) {
      const practice = createDailyEvent(currentTime, school);
      practice.title = `You go to ${activity.title} practice.`;
      practice.name = 'school';
      person.dailyPlan!.push(practice);
      currentTime = 16;
    }
  }

  // On the way home
  const goingHome = createDailyEvent(currentTime, school);
  goingHome.title = "You're on the way home";
  goingHome.name = 'school';
  person.dailyPlan!.push(goingHome);

  currentTime += 1;
  const arriveHome = createDailyEvent(currentTime, home);
  arriveHome.title = pickVariant(arriveHomeVariants);
  arriveHome.name = 'home';
  person.dailyPlan!.push(arriveHome);

  currentTime += 1;
  const dinner = createDailyEvent(currentTime, home);
  dinner.title = pickVariant(eatDinnerVariants);
  dinner.name = 'dinner';
  person.dailyPlan!.push(dinner);

  currentTime += 1;
  const relax = createDailyEvent(currentTime, home);
  relax.title = 'You relax at home';
  relax.name = 'relax';
  person.dailyPlan!.push(relax);

  const bedTime = randomInt(23, 24);
  const bed = createDailyEvent(bedTime, home);
  bed.title = pickVariant(goToSleepVariants);
  bed.name = 'sleep';
  person.dailyPlan!.push(bed);
}

/**
 * Decay familiarity for all NPCs each day
 * Called from game loop at start of each day
 */
export function decayFamiliarity(player: Player, decayAmount: number = 3): void {
  for (const person of player.r ?? []) {
    if (person.status === 'alive' && (person.familiarity ?? 0) > 0) {
      person.familiarity = Math.max(0, (person.familiarity ?? 0) - decayAmount);
    }
  }
}

/**
 * Restore energy for a person (called at start of each day)
 * Energy restoration varies based on sleep quality and other factors
 */
export function restoreEnergy(person: Person, baseRestore: number = 1): void {
  if ((person.energy ?? 0) < 100) {
    person.energy = Math.min(100, (person.energy ?? 0) + baseRestore);
  }
}

/**
 * Get peak energy for a person based on age and health
 * Used for calculating energy restoration rates
 */
export function getPeakEnergy(person: Person): number {
  const baseEnergy = 100;
  const age = person.ageYears ?? 0;
  const health = person.health ?? 100;

  // Energy peak decreases with age
  let agePenalty = 0;
  if (age > 50) {
    agePenalty = Math.floor((age - 50) / 5);
  }

  // Health affects peak energy
  const healthModifier = health / 100;

  return Math.max(50, Math.floor((baseEnergy - agePenalty) * healthModifier));
}

/**
 * Update bio based on current activity and mood
 * Called periodically to generate dynamic bio text
 */
export function updateBio(person: Person): void {
  const parts: string[] = [];

  // Add occupation
  if (person.occupation) {
    if (person.occupation === 'student') {
      parts.push('Student');
    } else if (person.occupation === 'preschool') {
      parts.push('Child');
    } else if (person.occupation === 'retired') {
      parts.push('Retired');
    } else if (person.job) {
      const jobTitle = typeof person.job === 'object' && 'title' in person.job
        ? person.job.title
        : person.occupation;
      parts.push(jobTitle);
    }
  }

  // Add current activity from intraday message
  if (person.intraDayMessage) {
    parts.push(`Currently: ${person.intraDayMessage}`);
  }

  // Add mood if notable
  if (person.mood && person.mood !== 'Calm') {
    parts.push(`Feeling ${person.mood.toLowerCase()}`);
  }

  person.bio = parts.join(' | ');
}

/**
 * Check if current time matches a school day
 * Used for determining if student should be in school
 */
export function isSchoolDay(player: Player): boolean {
  // Not on weekends
  if (player.weekend) return false;

  // Not during summer vacation
  if (player.summerVacation) return false;

  // Check if within school hours (8-15)
  const hour = player.hourOfDay ?? 0;
  return hour >= 8 && hour <= 15;
}

/**
 * Get the current location type for a person
 */
export function getLocationType(player: Player, person: Person): string {
  const locationId = person.location;
  if (!locationId) return 'unknown';

  const location = findLocationById(player.l ?? [], locationId);
  return location?.type ?? 'unknown';
}
