/**
 * Health and Habits Management System
 * Handles health conditions, weight management, death mechanics, and habit tracking
 * Ported from Python health/health_manager.py
 */

import { Person, Sex } from '../../models/Person.js';
import { Player } from '../../models/Player.js';
import { getPlayerStatistics } from '../retention/statistics.js';
import { getEldestLivingChild } from '../character/character_manager.js';
import { MEAL_HUNGER_REDUCTION, MEAL_THIRST_REDUCTION } from '../../game/economyConstants.js';

// ============================================================
// Types and Interfaces
// ============================================================

export type WeightType = 'Underweight' | 'Normal' | 'Overweight' | 'Obese';
export type HabitType = 'positive' | 'negative';
export type HabitStatus = 'active' | 'quitting';

export interface HealthCondition {
  id: string;
  title: string;
  healthModifier: number;
  averageDuration: number;
  date: string | null;
  description: string;
  image?: string;
  isCured?: boolean;
}

export interface Habit {
  name: string;
  description: string;
  type: 'habit';
  habitType: HabitType;
  status: HabitStatus;
  quitProgress: number;
}

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

/**
 * Recalculate peak energy for a person.
 * This is a local helper that mirrors the getPeakEnergy function
 * from stats_manager to avoid circular imports.
 *
 * Peak energy calculation:
 * - Base: 0
 * - Per quitting habit: +5 energy
 * - Activity energy modifiers
 * - Job level energy modifiers
 * - Focus energy modifiers
 *
 * @param person - Person object to recalculate energy for
 */
function recalculatePeakEnergy(person: Person): void {
  let peakEnergy = 0;
  const personAny = person as unknown as { habits?: Habit[] };

  // Quitting habits cost energy (+5 per quitting habit)
  if (personAny.habits) {
    for (const habit of personAny.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 energy modifiers
        if (record.focus) {
          const focusModifiers: Record<string, number> = {
            'Balanced': 0,
            'Intensive': 10,
            'Relaxed': -5,
            'Academic': 5,
            'Social': 3,
          };
          peakEnergy += focusModifiers[record.focus] ?? 0;
        }
      }
    }
  }

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

// ============================================================
// Weight Management
// ============================================================

/**
 * Determine weight category based on weight value
 */
export function getWeightType(weight: number): WeightType {
  if (weight < 50) {
    return 'Underweight';
  } else if (weight < 70) {
    return 'Normal';
  } else if (weight < 90) {
    return 'Overweight';
  } else {
    return 'Obese';
  }
}

/**
 * Enforce weight boundaries (0-100)
 */
export function handleWeight(person: Person): void {
  const personAny = person as unknown as { weight?: number };
  if (personAny.weight !== undefined) {
    if (personAny.weight < 0) {
      personAny.weight = 0;
    }
    if (personAny.weight > 100) {
      personAny.weight = 100;
    }
  }
}

// ============================================================
// Health Management
// ============================================================

/**
 * Update person's health based on weight type and health conditions
 */
export function handleHealth(player: Player, person: Person): void {
  const personAny = person as unknown as {
    weightType?: WeightType;
    health?: number;
    healthConditions?: HealthCondition[];
    deathChance?: number;
  };

  // Health impact from weight
  if (personAny.weightType === 'Underweight' || personAny.weightType === 'Obese') {
    personAny.health = (personAny.health ?? 100) - 0.000001;
  }

  // Process health conditions
  if (personAny.healthConditions && personAny.healthConditions.length > 0) {
    const playerDate = parseDate(player.date);

    for (const condition of personAny.healthConditions) {
      if (condition.date) {
        const conditionDate = parseDate(condition.date);
        const healingDate = new Date(conditionDate);
        healingDate.setDate(healingDate.getDate() + condition.averageDuration * 7);

        if (playerDate >= healingDate) {
          condition.isCured = true;
        } else {
          personAny.health = (personAny.health ?? 100) - condition.healthModifier / 100000;
          condition.isCured = false;
        }
      }
    }
  }

  // Death chance increases when health is very low
  if ((personAny.health ?? 100) <= 0) {
    personAny.deathChance = (personAny.deathChance ?? 0) + 1;
  }

  // Keep health in [0, 100]. (Was a latent landmine: `if (health >= 2) health = 2`
  // slammed every character's health to 2 — which is why this function was never
  // wired into the live loops. Lifestyle/illness mortality now lives in
  // updateDeathChance; if this condition-drain path is ever wired, this is a
  // correct clamp instead of a near-death cap.)
  personAny.health = Math.max(0, Math.min(100, personAny.health ?? 100));
}

/**
 * Parse MM-DD date format
 */
function parseDate(dateStr: string): Date {
  const [month, day] = dateStr.split('-').map((n) => parseInt(n, 10));
  const date = new Date();
  date.setMonth(month - 1);
  date.setDate(day);
  return date;
}

/**
 * A summary of a completed life, built at the moment of death. Persisted on the
 * player and surfaced to the client (via lifeSummaryEvent online, or the
 * persisted field on next reconnect for offline deaths) to power the
 * end-of-life replay hook / New Life flow.
 */
export interface LifeSummary {
  /** Final age in years at time of death. */
  finalAge: number;
  /** Net worth / money on hand at death. */
  netWorth: number;
  /** Highest career reached (title + best income observed for that role). */
  peakCareer: {
    title: string;
    bestIncome: number;
  } | null;
  /** Number of NPC relationships the player accumulated. */
  relationshipsCount: number;
  /** Number of children. */
  childrenCount: number;
  /** A few notable events pulled from the life/message log. */
  notableEvents: string[];
  /** Count of achievements earned this life. */
  achievementsEarned: number;
  /** Lifetime earnings across the whole life (from statistics). */
  lifetimeEarnings: number;
  /** Computed life score. See LIFE_SCORE_WEIGHTS for the formula. */
  score: number;
  /** True if this life's score beat the player's previous best (hall of fame). */
  isNewRecord?: boolean;
  /** The player's best score across all lives (after this one is counted). */
  bestScore?: number;
  /** Which life this was (1 = first life), from the cross-life hall of fame. */
  lifeNumber?: number;
  /** ISO timestamp the summary was generated. */
  diedAt: string;
  /**
   * Generational legacy payload computed at death. Drives the death screen's
   * "Continue as <heir>" option and seeds the next life. Null only on the
   * (impossible) path where the player has no character.
   */
  legacy: LegacyData;
}

// ============================================================
// Generational / Legacy System
// ============================================================

/**
 * One ancestor's record in the persistent family tree. Appended on each death
 * and carried forward across the per-life wipe so the heir inherits a growing
 * lineage history.
 */
export interface FamilyTreeEntry {
  /** Full name of the ancestor at death. */
  name: string;
  /** Sex of the ancestor. */
  sex: string;
  /** Final age in years at death. */
  finalAge: number;
  /** Peak career title reached, or null if none. */
  peakCareer: string | null;
  /** Life score for this ancestor. */
  score: number;
  /** Net worth at death. */
  netWorth: number;
  /** ISO timestamp of death. */
  diedAt: string;
  /** 1-based generation index (1 = founder/oldest recorded). */
  generation: number;
}

/**
 * The heir designate surfaced to the client so the death screen can offer
 * "Continue as <name>". Null when the line has no living child to continue.
 */
export interface HeirInfo {
  /** Relationship id of the heir (the eldest living child). */
  id: string;
  /** Heir's full name. */
  name: string;
  /** Heir's sex. */
  sex: Sex;
  /** Heir's age in years at the moment of inheritance. */
  ageYears: number;
  /** Heir's affinity toward the deceased (drives the inheritance bonus). */
  affinity: number;
}

/**
 * Legacy payload built at death and embedded in the LifeSummary. Tells the
 * client whether a heir exists and how much wealth + prestige carries forward.
 */
export interface LegacyData {
  /** The heir (eldest living child) or null if the line ends. */
  heir: HeirInfo | null;
  /** Amount of money transferred to the next life (post estate-tax, +affinity bonus). */
  inheritance: number;
  /** Compounding family prestige AFTER this life's contribution is folded in. */
  familyPrestige: number;
  /** This life's contribution to family prestige. */
  prestigeGained: number;
  /** The full persisted family tree (ancestors + this life appended). */
  familyTree: FamilyTreeEntry[];
}

/**
 * Fraction of the deceased's positive net worth that survives the "estate tax"
 * and is available for inheritance. The remainder is lost to taxes/probate.
 */
export const ESTATE_TAX_RETENTION = 0.5;

/**
 * Maximum additional inheritance multiplier granted by a maximally-loving heir.
 * The relationship audit's guidance: inheritance favors high-affinity family.
 * Heir affinity in [-100, 100] maps to a multiplier in [1.0, 1 + this].
 */
export const HEIR_AFFINITY_BONUS_MAX = 0.2;

/**
 * Divisor converting a life score into family-prestige points. A score of ~100
 * adds 1 prestige, so prestige accrues slowly and compounds across many lives.
 */
export const PRESTIGE_SCORE_DIVISOR = 100;

/**
 * Starting money advantage per point of family prestige, seeded into the heir's
 * next life on top of the direct inheritance.
 */
export const PRESTIGE_MONEY_SEED = 100;

/**
 * Compute the inheritance amount for an heir from a deceased character.
 * 50% of positive net worth survives the estate tax, then a high-affinity heir
 * gets up to +20% more (affinity 100 => x1.2; affinity <= 0 => no bonus).
 * Returns 0 when there is no heir or no positive estate.
 */
export function computeInheritance(netWorth: number, heir: HeirInfo | null): number {
  if (!heir) return 0;
  const estate = Math.max(0, netWorth) * ESTATE_TAX_RETENTION;
  const affinity = Math.max(0, Math.min(100, heir.affinity));
  const bonus = 1 + (affinity / 100) * HEIR_AFFINITY_BONUS_MAX;
  return Math.round(estate * bonus);
}

/**
 * Convert a life score into the family-prestige points it contributes. Floored
 * at 0 so a disastrous life never erodes accumulated lineage prestige.
 */
export function computePrestigeGain(score: number): number {
  return Math.max(0, Math.floor(score / PRESTIGE_SCORE_DIVISOR));
}

/**
 * Build the family-tree entry for the life that just ended.
 */
function buildFamilyTreeEntry(
  player: Player,
  summary: Omit<LifeSummary, 'legacy'>,
  generation: number
): FamilyTreeEntry {
  const c = player.c;
  return {
    name: c ? `${c.firstname} ${c.lastname}`.trim() : 'Unknown',
    sex: c?.sex ?? 'Male',
    finalAge: summary.finalAge,
    peakCareer: summary.peakCareer?.title ?? null,
    score: summary.score,
    netWorth: summary.netWorth,
    diedAt: summary.diedAt,
    generation,
  };
}

/**
 * Compute the generational legacy payload at death. Pure aside from reading the
 * player. Appends this life to the (persisted) family tree, picks the eldest
 * living child as heir, computes the inheritance, and folds this life's score
 * into the compounding family prestige.
 */
export function buildLegacy(
  player: Player,
  summary: Omit<LifeSummary, 'legacy'>
): LegacyData {
  const eldest = getEldestLivingChild(player);
  const heir: HeirInfo | null = eldest
    ? {
        id: eldest.id,
        name: `${eldest.firstname} ${eldest.lastname}`.trim(),
        sex: eldest.sex,
        ageYears: eldest.ageYears ?? 0,
        affinity: eldest.affinity ?? 50,
      }
    : null;

  const inheritance = computeInheritance(summary.netWorth, heir);
  const prestigeGained = computePrestigeGain(summary.score);
  const familyPrestige = (player.familyPrestige ?? 0) + prestigeGained;

  const nextGeneration = (player.familyTree?.length ?? 0) + 1;
  const entry = buildFamilyTreeEntry(player, summary, nextGeneration);
  const familyTree = [...(player.familyTree ?? []), entry];

  return {
    heir,
    inheritance,
    familyPrestige,
    prestigeGained,
    familyTree,
  };
}

/**
 * Weighting constants for the life score. The score is a single positive
 * number that rewards a richer, longer, more accomplished life so it scales
 * monotonically: more wealth, higher career, more relationships/children,
 * greater longevity, and more achievements all strictly increase the score.
 *
 * score =
 *     longevity   * finalAge
 *   + wealth      * (netWorth, floored at 0) / 1000
 *   + earnings    * lifetimeEarnings / 1000
 *   + career      * peakCareer.bestIncome / 100
 *   + relationships * relationshipsCount
 *   + children    * childrenCount
 *   + achievement * achievementsEarned
 */
export const LIFE_SCORE_WEIGHTS = {
  longevity: 10,
  wealth: 1,
  earnings: 1,
  career: 5,
  relationships: 25,
  children: 100,
  achievement: 50,
} as const;

/**
 * Derive the peak career (title + best income) from a person's current job and
 * job levels. Uses the richest salary level on the held occupation.
 */
function derivePeakCareer(person: Person): LifeSummary['peakCareer'] {
  const job = person.job as
    | { title?: string; levels?: Array<{ salary?: number }> }
    | null
    | undefined;
  if (!job || !job.title) return null;

  const levelSalaries = Array.isArray(job.levels)
    ? job.levels.map((l) => l?.salary ?? 0)
    : [];
  const bestIncome = Math.max(
    person.salary ?? 0,
    ...(levelSalaries.length > 0 ? levelSalaries : [0])
  );

  return { title: job.title, bestIncome };
}

/**
 * Pick a few notable events from the player's message/life log. Prefers the
 * most recent meaningful lines (graduation, marriage, children, career, etc.)
 * and always includes the death line last.
 */
function deriveNotableEvents(player: Player): string[] {
  const log = Array.isArray(player.messageLog) ? player.messageLog : [];
  const NOTABLE = /(graduat|married|engag|promot|hired|born|adopt|scholarship|award|inherit|lottery|retire|business)/i;
  const highlights = log.filter((m) => typeof m === 'string' && NOTABLE.test(m));
  // Keep up to the last 5 highlights, preserving chronological order.
  return highlights.slice(-5);
}

/**
 * Compute the weighted life score from a (partial) summary. Exported for tests
 * and so callers can reason about monotonicity.
 */
export function computeLifeScore(
  summary: Pick<
    LifeSummary,
    | 'finalAge'
    | 'netWorth'
    | 'lifetimeEarnings'
    | 'peakCareer'
    | 'relationshipsCount'
    | 'childrenCount'
    | 'achievementsEarned'
  >
): number {
  const w = LIFE_SCORE_WEIGHTS;
  const wealth = Math.max(0, summary.netWorth);
  const careerIncome = summary.peakCareer?.bestIncome ?? 0;

  const score =
    w.longevity * Math.max(0, summary.finalAge) +
    w.wealth * (wealth / 1000) +
    w.earnings * (Math.max(0, summary.lifetimeEarnings) / 1000) +
    w.career * (careerIncome / 100) +
    w.relationships * Math.max(0, summary.relationshipsCount) +
    w.children * Math.max(0, summary.childrenCount) +
    w.achievement * Math.max(0, summary.achievementsEarned);

  return Math.round(score);
}

/**
 * Build the life summary for a player at the moment of death. Pure (no side
 * effects beyond reading the player / statistics) so it can be reused/tested.
 */
export function buildLifeSummary(player: Player): LifeSummary {
  const c = player.c;
  const stats = getPlayerStatistics(player.userId);

  const finalAge = c?.ageYears ?? 0;
  const netWorth = c?.money ?? player.money ?? 0;
  const peakCareer = c ? derivePeakCareer(c) : null;
  const relationshipsCount = Array.isArray(player.r) ? player.r.length : 0;
  const childrenCount = Array.isArray(c?.children) ? c!.children.length : 0;
  const notableEvents = deriveNotableEvents(player);
  // Achievements earned this life = total acknowledged + unacknowledged unlocked.
  // We approximate via statistics where available; fall back to 0.
  const achievementsEarned = (stats as unknown as { achievementsEarned?: number })
    .achievementsEarned ?? 0;
  const lifetimeEarnings = stats.lifetimeEarnings ?? 0;

  const partial = {
    finalAge,
    netWorth,
    lifetimeEarnings,
    peakCareer,
    relationshipsCount,
    childrenCount,
    achievementsEarned,
  };

  const summaryWithoutLegacy: Omit<LifeSummary, 'legacy'> = {
    ...partial,
    notableEvents,
    score: computeLifeScore(partial),
    diedAt: new Date().toISOString(),
  };

  return {
    ...summaryWithoutLegacy,
    legacy: buildLegacy(player, summaryWithoutLegacy),
  };
}

/**
 * Handle player character death event — the SINGLE source of truth for death.
 *
 * Both the online death path (PlayerSession.processDayTick) and the offline
 * death path (GameEngine) call this so death behaves identically everywhere:
 *  - queues the "You have died!" message
 *  - sets controller to 'inactive'
 *  - sets character status to 'dead'
 *  - builds + persists player.lifeSummary (score + replay hook payload)
 *
 * Idempotent: if a summary already exists (e.g. handleDeath called twice across
 * the offline runGameTick guard), it is not rebuilt.
 */
export function handleDeath(player: Player): void {
  player.controller = 'inactive';
  if (player.c) {
    player.c.status = 'dead';
  }

  // Dedup the death message so repeated death handling doesn't spam the queue.
  if (!player.events.has('death')) {
    player.messageQueue.push('You have died!');
    player.events.add('death');
  }

  // Build the life summary once.
  if (!player.lifeSummary) {
    player.lifeSummary = buildLifeSummary(player);

    // Persist the generational layer ONTO THE PLAYER so it survives the
    // per-life wipe. familyPrestige + familyTree are intentionally NOT cleared
    // by startNewLife's fresh/heir branches except when the player explicitly
    // chooses a fresh start. The pending inheritance is stashed so the heir
    // path can apply it after the wipe (the deceased's money is gone once we
    // reset the character).
    const legacy = player.lifeSummary.legacy;
    player.familyPrestige = legacy.familyPrestige;
    player.familyTree = legacy.familyTree;
    player.pendingInheritance = legacy.inheritance;

    // Update the cross-life Hall of Fame (the "beat your best life" hook). This
    // survives EVERY new life — including a deliberate fresh start — so death
    // always has stakes: "new record!" or "best: X". Stamp the comparison onto
    // the summary so the death screen can show it.
    const hof = player.hallOfFame ?? { bestScore: 0, lives: 0, recentScores: [], bestLife: null };
    const score = player.lifeSummary.score;
    const isNewRecord = hof.lives === 0 ? true : score > hof.bestScore;
    hof.lives += 1;
    hof.recentScores = [...(hof.recentScores ?? []), score].slice(-5);
    if (score > hof.bestScore || hof.lives === 1) {
      hof.bestScore = score;
      const c = player.c;
      hof.bestLife = {
        score,
        finalAge: player.lifeSummary.finalAge,
        name: c ? `${c.firstname ?? ''} ${c.lastname ?? ''}`.trim() : '',
        diedAt: player.lifeSummary.diedAt,
      };
    }
    player.hallOfFame = hof;

    player.lifeSummary.isNewRecord = isNewRecord;
    player.lifeSummary.bestScore = hof.bestScore;
    player.lifeSummary.lifeNumber = hof.lives;

    if (isNewRecord && hof.lives > 1) {
      player.messageQueue.push(`New personal best! Life score ${score} beats your previous best.`);
    }
  }
}

/**
 * Calculate death chance based on age and health.
 * This follows the Python implementation:
 * - Base death chance increases with age brackets
 * - Final chance is divided by health (lower health = higher death chance)
 *
 * deathChance is a per-in-game-DAY probability (checkDeath fires when
 * deathChance > Math.random()). At full health (100) the effective daily
 * probability is baseChange / 100; lower health multiplies it up.
 *
 * The original curve was calibrated for a world where starvation killed every
 * character at age ~29-40, so the age brackets themselves were near-zero
 * (e.g. 70-82 was 6e-8/day at full health → essentially immortal). Once the
 * meal sink (T003) removed force-starvation, that flat curve left EVERY healthy
 * life pinned to the 120-year hard cap — a new pathology in the other
 * direction. These brackets re-calibrate so a HEALTHY character dies of natural
 * causes in a believable old-age band (median ~80, spread ~70-95), while low
 * health still sharply accelerates death (the /health term).
 *
 * Approx annual mortality at full health (1-(1-baseChance/100)^365):
 * - Age 20-30: ~0.1%/yr
 * - Age 30-40: ~0.2%/yr
 * - Age 40-50: ~0.5%/yr
 * - Age 50-60: ~1.1%/yr
 * - Age 60-70: ~3.2%/yr
 * - Age 70-82: ~8.4%/yr (noticeable increase)
 * - Age 82-100: ~26%/yr (significant increase)
 * - Age 100+: ~60%/yr (high probability; hard cap still at 120)
 *
 * @param person - Person object with age and health attributes
 * @returns Updated death chance value
 */
export function updateDeathChance(person: Person): number {
  let baseChance = 0;

  if (person.ageYears > 100) {
    baseChance = 0.25;
  } else if (person.ageYears > 82) {
    baseChance = 0.082;
  } else if (person.ageYears > 70) {
    baseChance = 0.024;
  } else if (person.ageYears > 60) {
    baseChance = 0.009;
  } else if (person.ageYears > 50) {
    baseChance = 0.003;
  } else if (person.ageYears > 40) {
    baseChance = 0.0014;
  } else if (person.ageYears > 30) {
    baseChance = 0.0006;
  } else if (person.ageYears > 20) {
    baseChance = 0.0003;
  }

  // Divide by health (lower health = higher death chance)
  // Health defaults to 1 (100%) but can be lower due to conditions
  // Prevent division by zero or negative health
  const health = person.health > 0 ? person.health : 0.01;

  // Lifestyle / illness mortality: chronic conditions and active vices raise the
  // death risk, so a life can end from something other than old age — a heavy
  // smoker or someone with heart disease dies measurably younger on average.
  // Bounded and age-weighted (it multiplies the small age base), so it adds
  // variety in mid/late life without threatening the young. Acute conditions are
  // temporary nuisances and deliberately do NOT feed mortality here.
  let riskMultiplier = 1;
  const conditions =
    (person as unknown as { healthConditions?: HealthCondition[] }).healthConditions ?? [];
  let chronicCount = 0;
  for (const c of conditions) {
    if (c.date && !c.isCured && c.averageDuration >= 99999) chronicCount++;
  }
  const activeVices = (person.habits ?? []).filter(
    (h) => (h as { habitType?: string }).habitType === 'negative' && h.status === 'active'
  ).length;
  riskMultiplier += 0.5 * chronicCount + 0.25 * activeVices;

  person.deathChance = (baseChance / health) * riskMultiplier;

  return person.deathChance;
}

/**
 * Check if a person should die based on their death chance.
 * Called once per in-game day for each character.
 *
 * @param person - Person to check
 * @returns true if the person dies, false otherwise
 */
export function checkDeath(person: Person): boolean {
  const deathChance = person.deathChance ?? 0;

  // Guaranteed death at 120+ years old (matches Python)
  if (person.ageYears > 120) {
    return true;
  }

  // Probabilistic death based on deathChance
  // Python: if item.deathChance * item.health * 100 > (random.random() * 100)
  // For player: if player.c.deathChance * 100 > (random.random() * 100)
  return deathChance * 100 > Math.random() * 100;
}

/**
 * Result of a death check for a relationship person
 */
export interface RelationshipDeathResult {
  person: Person;
  index: number;
  message: string;
}

/**
 * Check death for all relationship persons (family members, friends, etc.)
 * Updates their death chance on birthday and checks if they die.
 * Matches Python's updateAge() behavior for relationships.
 *
 * @param player - Player object with relationships array
 * @returns Array of death results for any characters that died
 */
export function checkRelationshipDeaths(player: Player): RelationshipDeathResult[] {
  const deaths: RelationshipDeathResult[] = [];

  if (!player.r || player.r.length === 0) {
    return deaths;
  }

  for (let index = 0; index < player.r.length; index++) {
    const person = player.r[index];

    // Skip already dead or non-alive characters
    if (person.status !== 'alive') {
      continue;
    }

    // Check death using the combined formula from Python:
    // if item.deathChance * item.health * 100 > (random.random() * 100) or item.ageYears > 120
    const deathChance = person.deathChance ?? 0;
    const health = person.health ?? 1;
    const shouldDie = (deathChance * health * 100 > Math.random() * 100) || person.ageYears > 120;

    if (shouldDie) {
      // Mark as dead
      person.status = 'dead';

      // Get title for message (e.g., "mother", "father", "grandmother")
      const title = (person as unknown as { title?: string }).title;
      const firstname = person.firstname?.charAt(0).toUpperCase() + person.firstname?.slice(1);
      const lastname = person.lastname?.charAt(0).toUpperCase() + person.lastname?.slice(1);

      // Generate death message matching Python format
      let message: string;
      if (title) {
        message = `Your ${title} ${firstname} ${lastname} has died at the age of ${person.ageYears} years old.`;
      } else {
        message = `${firstname} ${lastname} has died at the age of ${person.ageYears} years old.`;
      }

      deaths.push({
        person,
        index,
        message,
      });
    }
  }

  return deaths;
}

/**
 * Update age for all relationship characters and check for deaths.
 * This should be called once per in-game day (like Python's updateAge).
 *
 * @param player - Player object
 * @returns Array of death messages for any characters that died
 */
export function updateRelationshipAges(player: Player): string[] {
  const deathMessages: string[] = [];

  if (!player.r || player.r.length === 0) {
    return deathMessages;
  }

  for (let index = 0; index < player.r.length; index++) {
    const person = player.r[index];

    // Skip non-alive characters
    if (person.status !== 'alive') {
      continue;
    }

    // Increment age days (once per in-game day)
    person.ageDays = (person.ageDays ?? 0) + 1;

    // Affinity decay for adult player (matches Python)
    if (player.c.ageYears > 18 && person.ageDays % 30 === 0) {
      person.affinity = (person.affinity ?? 50) - 1;
    }

    // Check for birthday (every 365 days)
    if (person.ageDays % 365 === 0) {
      person.ageYears = (person.ageYears ?? 0) + 1;
      person.affinity = (person.affinity ?? 50) - 1;

      // Update death chance on birthday
      updateDeathChance(person);
    }

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

  // Check for deaths (separate pass to avoid modifying while iterating)
  const deaths = checkRelationshipDeaths(player);
  for (const death of deaths) {
    deathMessages.push(death.message);
  }

  return deathMessages;
}

/**
 * Get the title for a relationship person based on their relationships array.
 * Used for death messages and other notifications.
 *
 * @param person - The relationship person
 * @returns Title string (e.g., "mother", "father", "grandmother") or empty string
 */
export function getRelationshipTitle(person: Person): string {
  const personAny = person as unknown as { title?: string };
  return personAny.title ?? '';
}

// ============================================================
// Hunger and Thirst Management
// ============================================================

/**
 * Enforce hunger and thirst boundaries (minimum 0)
 */
export function handleHunger(person: Person): Person {
  const personAny = person as unknown as { hunger?: number; thirst?: number };

  if (personAny.hunger !== undefined && personAny.hunger < 0) {
    personAny.hunger = 0;
  }
  if (personAny.thirst !== undefined && personAny.thirst < 0) {
    personAny.thirst = 0;
  }

  return person;
}

/**
 * Apply a single meal's hunger/thirst reduction to a person, mutating in place.
 *
 * This is the SINGLE shared sink for hunger/thirst. It is called from the meal
 * branch of getIntradayActivity (the daily-plan executor that BOTH the online
 * PlayerSession loop and the offline LoopManager/GameEngine loop run), so the
 * two paths can never diverge. The reduction amounts live in economyConstants
 * alongside applyHourlySurvival so the feed and the drain share one source of
 * truth. Values are clamped at 0 (you can't be "negatively hungry").
 */
export function applyMealEffect(person: Person): void {
  if (!person) return;
  const personAny = person as unknown as { hunger?: number; thirst?: number };
  personAny.hunger = Math.max(0, (personAny.hunger ?? 0) - MEAL_HUNGER_REDUCTION);
  personAny.thirst = Math.max(0, (personAny.thirst ?? 0) - MEAL_THIRST_REDUCTION);
}

/**
 * Process meal event, reducing hunger and thirst for the player character.
 * Thin wrapper over the shared applyMealEffect sink so all meal handling routes
 * through one implementation.
 */
export function mealEvent(player: Player): Player {
  if (player.c) {
    applyMealEffect(player.c);
  }
  return player;
}

// ============================================================
// Health Conditions System
// ============================================================

/**
 * Create a new health condition
 */
export function createHealthCondition(
  id: string,
  title: string,
  healthModifier: number,
  averageDuration: number,
  description: string,
  image?: string
): HealthCondition {
  return {
    id,
    title,
    healthModifier,
    averageDuration,
    date: null,
    description,
    image,
    isCured: false,
  };
}

/**
 * Get list of all available health conditions
 */
export function getHealthConditions(): HealthCondition[] {
  return [
    createHealthCondition('condition1', 'COVID-19', 10, 14, 'You have contracted COVID-19. You must quarantine for 14 days.'),
    createHealthCondition('condition2', 'Common Cold', 5, 3, 'You have a common cold. You must rest for 3 days.'),
    createHealthCondition('condition3', 'Flu', 10, 7, 'You have the flu. You must rest for 7 days.'),
    createHealthCondition('condition4', 'Broken Bone', 15, 30, 'You have a broken bone. You must rest for 30 days.'),
    createHealthCondition('condition5', 'Sprained Ankle', 5, 7, 'You have a sprained ankle. You must rest for 7 days.'),
    createHealthCondition('condition6', 'Food Poisoning', 5, 3, 'You have food poisoning. You must rest for 3 days.'),
    createHealthCondition('condition7', 'Diabetes', 5, 99999, 'You have been diagnosed with Diabetes. Regular medication and lifestyle changes are required.'),
    createHealthCondition('condition8', 'High Blood Pressure', 5, 99999, 'You have high blood pressure. You need to monitor it regularly and possibly take medication.'),
    createHealthCondition('condition9', 'Migraine', 7, 3, 'You are suffering from a migraine. You need rest and possibly medication to manage the symptoms.'),
    createHealthCondition('condition10', 'Asthma', 2, 99999, 'You have been diagnosed with Asthma. Regular use of inhalers and avoiding triggers is required.'),
    createHealthCondition('condition11', 'Heart Disease', 20, 99999, 'You have been diagnosed with heart disease. Lifestyle changes, medication, or surgery may be required.'),
    createHealthCondition('condition15', 'Arthritis', 10, 99999, 'You have been diagnosed with Arthritis. Pain management and physical therapy can help.'),
    createHealthCondition('condition16', 'Gastroenteritis', 7, 5, 'You have gastroenteritis. Rest, fluid intake, and possibly medication are required.'),
  ];
}

/**
 * Apply a health condition to a person
 */
export function applyHealthCondition(player: Player, person: Person, conditionId: string): void {
  const conditions = getHealthConditions();
  const condition = conditions.find((c) => c.id === conditionId);

  if (condition) {
    condition.date = player.date;
    const personAny = person as unknown as { healthConditions?: HealthCondition[] };
    personAny.healthConditions = personAny.healthConditions ?? [];
    personAny.healthConditions.push(condition);

    player.messageQueue.push(condition.description);
  }
}

// ============================================================
// Habits System
// ============================================================

/**
 * Create a habit
 */
export function createHabit(
  name: string,
  description: string,
  habitType: HabitType = 'negative'
): Habit {
  return {
    name,
    description,
    type: 'habit',
    habitType,
    status: 'active',
    quitProgress: 0,
  };
}

/**
 * Negative habit definitions
 */
export const negativeHabits: Record<string, Habit> = {
  tardiness: createHabit('tardiness', 'Your lack of punctuality is becoming noticeable.'),
  nail_biting: createHabit('nail_biting', 'You catch yourself biting your nails. It\'s a nervous habit.'),
  overthinking: createHabit('overthinking', 'You can\'t help but overanalyze every situation.'),
  procrastination: createHabit('procrastination', 'You tend to put things off until the last minute.'),
  negative_thinking: createHabit('negative_thinking', 'You\'re struggling to keep negative thoughts at bay.'),
  overeating: createHabit('overeating', 'Your tiredness is making you reach for comfort foods. You\'re overeating and it\'s not making you feel any better.'),
  under_exercising: createHabit('under_exercising', 'You haven\'t been getting much exercise lately.'),
  excessive_screen_time: createHabit('excessive_screen_time', 'You\'re spending too much time in front of screens.'),
  impulsiveness: createHabit('impulsiveness', 'You have a tendency to act without thinking things through.'),
  indecisiveness: createHabit('indecisiveness', 'You struggle with making decisions, big or small.'),
  neglecting_self_care: createHabit('neglecting_self_care', 'You haven\'t been taking care of yourself as much as you should.'),
  poor_time_management: createHabit('poor_time_management', 'You\'re always running behind schedule.'),
  overeating_when_stressed: createHabit('overeating_when_stressed', 'Stress pushes you to eat more than you need.'),
  excessive_caffeine_intake: createHabit('excessive_caffeine_intake', 'You\'ve been consuming too much caffeine.'),
  smoking: createHabit('smoking', 'The habit of smoking is taking a toll on your health.'),
  excessive_alcohol_consumption: createHabit('excessive_alcohol_consumption', 'Exhaustion sets in and you find yourself reaching for a drink more often than usual.'),
};

/**
 * Positive habit definitions
 */
export const positiveHabits: Record<string, Habit> = {
  punctuality: createHabit('punctuality', 'Your punctuality is appreciated by those around you.', 'positive'),
  regular_exercise: createHabit('regular_exercise', 'Your commitment to regular exercise is paying off.', 'positive'),
  healthy_eating: createHabit('healthy_eating', 'You feel better when you eat well.', 'positive'),
  positive_thinking: createHabit('positive_thinking', 'Your positive mindset helps you overcome challenges.', 'positive'),
  planning_ahead: createHabit('planning_ahead', 'Planning ahead makes life smoother.', 'positive'),
  prioritizing_self_care: createHabit('prioritizing_self_care', 'Taking care of yourself improves your mood.', 'positive'),
  effective_time_management: createHabit('effective_time_management', 'Your time management skills keep things under control.', 'positive'),
  practicing_gratitude: createHabit('practicing_gratitude', 'Practicing gratitude makes you feel happier.', 'positive'),
  mindfulness_meditation: createHabit('mindfulness_meditation', 'Meditation helps keep your mind clear.', 'positive'),
  regular_sleeping_schedule: createHabit('regular_sleeping_schedule', 'Maintaining a regular sleep schedule keeps you refreshed.', 'positive'),
  active_listening: createHabit('active_listening', 'You understand others better by actively listening.', 'positive'),
  showing_empathy: createHabit('showing_empathy', 'Showing empathy builds strong relationships.', 'positive'),
  tidy: createHabit('tidy', 'Keeping things tidy helps your peace of mind.', 'positive'),
  hygienic: createHabit('hygienic', 'Your hygiene habits keep you healthy.', 'positive'),
  work_life_balance: createHabit('work_life_balance', 'Balancing work and personal time keeps you satisfied.', 'positive'),
};

/**
 * Habit pairs (negative -> positive opposite)
 */
const habitPairs: Record<string, string> = {
  tardiness: 'punctuality',
  overthinking: 'positive_thinking',
  procrastination: 'planning_ahead',
  negative_thinking: 'positive_thinking',
  overeating: 'healthy_eating',
  under_exercising: 'regular_exercise',
  excessive_screen_time: 'work_life_balance',
  impulsiveness: 'planning_ahead',
  indecisiveness: 'planning_ahead',
  neglecting_self_care: 'prioritizing_self_care',
  poor_time_management: 'effective_time_management',
  overeating_when_stressed: 'healthy_eating',
  excessive_caffeine_intake: 'regular_sleeping_schedule',
  smoking: 'prioritizing_self_care',
  excessive_alcohol_consumption: 'prioritizing_self_care',
};

/**
 * Random sample from array
 */
function randomSample<T>(arr: T[], count: number): T[] {
  const shuffled = [...arr].sort(() => Math.random() - 0.5);
  return shuffled.slice(0, Math.min(count, arr.length));
}

/**
 * Initialize habits for a person based on age
 */
export function setHabits(person: Person): Person {
  const personAny = person as unknown as { habits?: Habit[] };

  let habitCount = 6;
  if (person.ageYears < 18) {
    habitCount = Math.round(person.ageYears / 5);
  }

  // Get random negative habits
  const negativeHabitValues = Object.values(negativeHabits).map((h) => ({ ...h }));
  const personNegativeHabits = randomSample(negativeHabitValues, Math.floor(Math.random() * (habitCount + 1)));

  habitCount = habitCount - personNegativeHabits.length;

  // Get non-conflicting positive habits
  const negativeHabitNames = personNegativeHabits.map((h) => h.name);
  const conflictingPositive = negativeHabitNames.map((name) => habitPairs[name]).filter(Boolean);

  const nonConflictingPositive = Object.values(positiveHabits)
    .map((h) => ({ ...h }))
    .filter((h) => !conflictingPositive.includes(h.name));

  const personPositiveHabits = randomSample(
    nonConflictingPositive,
    Math.min(Math.floor(Math.random() * (habitCount + 1)), nonConflictingPositive.length)
  );

  // Merge habits
  personAny.habits = [...personNegativeHabits, ...personPositiveHabits];

  return person;
}

/**
 * Start the process of quitting a habit
 * Sets the habit status to 'quitting' and recalculates peak energy
 * (quitting habits cost +5 energy per day while quitting)
 *
 * @param player - Player object with character and message queue
 * @param habitName - Name of the habit to quit (e.g., 'smoking', 'overeating')
 * @returns true if the habit was found and set to quitting, false otherwise
 */
export function quitHabit(player: Player, habitName: string): boolean {
  const personAny = player.c as unknown as { habits?: Habit[] };

  if (personAny.habits) {
    for (const habit of personAny.habits) {
      if (habit.name === habitName) {
        habit.status = 'quitting';
        const displayName = habitName.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
        player.messageQueue.push(`You have decided to try quitting "${displayName}".`);

        // Recalculate peak energy (quitting habits cost +5 energy)
        // This matches Python: getPeakEnergy(player.c) in quitHabit()
        recalculatePeakEnergy(player.c);

        return true;
      }
    }
  }
  return false;
}

/**
 * Stop trying to quit a habit and reset progress
 * Reverts the habit status to 'active' and resets quit progress to 0
 * Recalculates peak energy (no longer paying the +5 energy cost for quitting)
 *
 * @param player - Player object with character and message queue
 * @param habitName - Name of the habit to stop quitting
 * @returns true if the habit was found and reset, false otherwise
 */
export function stopQuitHabit(player: Player, habitName: string): boolean {
  const personAny = player.c as unknown as { habits?: Habit[] };

  if (personAny.habits) {
    for (const habit of personAny.habits) {
      if (habit.name === habitName) {
        habit.status = 'active';
        habit.quitProgress = 0;
        const displayName = habitName.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
        player.messageQueue.push(`You have decided to stop trying to quit "${displayName}".`);

        // Recalculate peak energy (no longer quitting, so no +5 energy cost)
        // This matches Python: getPeakEnergy(player.c) in stopQuitHabit()
        recalculatePeakEnergy(player.c);

        return true;
      }
    }
  }
  return false;
}

/**
 * Process habit quitting progress. After 30 days, habit is successfully quit.
 * Called once per week (on weekly tick) to increment quit progress.
 *
 * Habit quitting mechanics:
 * - Each week, quitProgress increments by 1 for habits with status 'quitting'
 * - After 30 weeks of quitting, the habit is removed completely
 * - Peak energy is recalculated when a habit is successfully quit
 * - The player is notified when a habit is successfully quit
 *
 * @param player - Player object for message queue
 * @param person - Person object with habits array
 * @returns Number of habits that were successfully quit
 */
export function handleHabitChanges(player: Player, person: Person): number {
  const personAny = person as unknown as { habits?: Habit[] };
  let quitCount = 0;

  if (personAny.habits) {
    for (let i = personAny.habits.length - 1; i >= 0; i--) {
      const habit = personAny.habits[i];
      if (habit.status === 'quitting') {
        habit.quitProgress += 1;
        console.log(`Habit: ${habit.name} progress: ${habit.quitProgress}/30`);

        if (habit.quitProgress >= 30) {
          // Get display name for the success message
          const displayName = habit.name.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());

          // Remove habit from list
          personAny.habits.splice(i, 1);
          player.messageQueue.push(`You have successfully quit "${displayName}"!`);

          // Recalculate peak energy (habit is gone, so no more +5 energy cost)
          // This matches Python: getPeakEnergy(person) in handleHabitChanges()
          recalculatePeakEnergy(person);

          quitCount++;
        }
      }
    }
  }

  return quitCount;
}

// ============================================================
// Bio Update (Weight/Hunger/Thirst)
// ============================================================

/**
 * Update player character's biological stats (hunger, thirst, weight)
 */
export function updateBio(player: Player): Player {
  const personAny = player.c as unknown as {
    weightType?: WeightType;
    hunger?: number;
    thirst?: number;
    weight?: number;
  };

  // Random change to hunger/thirst
  let top = 2;
  let bottom = -2;

  if (personAny.weightType === 'Underweight') {
    bottom = -4;
  }
  if (personAny.weightType === 'Obese') {
    top = 4;
  }

  personAny.hunger = (personAny.hunger ?? 50) + (Math.floor(Math.random() * (top - bottom + 1)) + bottom);
  personAny.thirst = (personAny.thirst ?? 50) + (Math.floor(Math.random() * (top - bottom + 1)) + bottom);

  handleHunger(player.c);

  personAny.weightType = personAny.weight !== undefined ? getWeightType(personAny.weight) : 'Normal';

  // Clamp hunger/thirst
  if ((personAny.hunger ?? 50) > 100) {
    personAny.hunger = 100;
  }
  if ((personAny.thirst ?? 50) > 100) {
    personAny.thirst = 100;
  }

  // Weight changes based on hunger
  if ((personAny.hunger ?? 50) < 2) {
    personAny.weight = (personAny.weight ?? 70) + 0.5;
  }
  if ((personAny.hunger ?? 50) > 98) {
    personAny.weight = (personAny.weight ?? 70) - 0.5;
  }

  handleHealth(player, player.c);
  handleWeight(player.c);

  return player;
}

// Export all functions
export const healthManager = {
  // Weight management
  getWeightType,
  handleWeight,
  // Health management
  handleHealth,
  // Death mechanics
  handleDeath,
  updateDeathChance,
  checkDeath,
  checkRelationshipDeaths,
  updateRelationshipAges,
  getRelationshipTitle,
  // Hunger/thirst
  handleHunger,
  mealEvent,
  // Health conditions
  getHealthConditions,
  applyHealthCondition,
  createHealthCondition,
  // Habits
  setHabits,
  quitHabit,
  stopQuitHabit,
  handleHabitChanges,
  createHabit,
  negativeHabits,
  positiveHabits,
  // Bio updates
  updateBio,
};
