/**
 * Player model - represents a player session in the game
 * Ported from Python Player class
 */

import { Person, RelationshipPerson } from './Person.js';
import { setAvatar } from '../services/character/avatar.js';

// Static game data imports - loaded once and included in playerObject like Python
import { getStoreItems, getInAppPurchaseItems } from '../services/shop/shop_manager.js';
import {
  getElementarySchools,
  getHighSchools,
  getColleges,
  getMajors,
  getFocuses,
  getExtracurriculars,
} from '../services/education/education_manager.js';
import { getHealthConditions, type LifeSummary, type FamilyTreeEntry } from '../services/health/health_manager.js';
import { getDateIdeas } from '../services/relationships/relationship_manager.js';
import { getOccupations } from '../services/jobs/job_manager.js';
// Import lifeGoals formatters DIRECTLY from the leaf module (not the retention
// barrel) to avoid a circular import: the retention index re-exports
// integration/tutorial which import this Player model. lifeGoals.ts only depends
// on diamondEconomy + statistics (neither imports Player), so this stays acyclic.
import {
  formatActiveGoals,
  formatCompletedGoals,
  getLifeScore,
} from '../services/retention/lifeGoals.js';

/**
 * Cross-life personal best. Persisted on the Player and NEVER wiped — survives
 * both a continue-as-heir and a deliberate fresh start, so the player always has
 * a "beat your best life" target across the whole save.
 */
export interface HallOfFame {
  /** Highest life score ever achieved on this save. */
  bestScore: number;
  /** Total completed lives (incremented at each death). */
  lives: number;
  /** Most recent life scores (oldest..newest), capped to the last few. */
  recentScores: number[];
  /** Snapshot of the best life, for the death/new-life screen. */
  bestLife: { score: number; finalAge: number; name: string; diedAt: string } | null;
}

// Static game data - cached for performance
let staticGameData: Record<string, unknown> | null = null;

function getStaticGameData(): Record<string, unknown> {
  if (!staticGameData) {
    staticGameData = {
      storeItems: getStoreItems(),
      inAppPurchases: getInAppPurchaseItems(),
      elementary_schools: getElementarySchools(),
      high_schools: getHighSchools(),
      colleges: getColleges(),
      majors: getMajors(),
      focuses: getFocuses(),
      extraCurriculars: getExtracurriculars(),
      healthConditions: getHealthConditions(),
      dateIdeas: getDateIdeas(),
      occupations: getOccupations(),
      moods: ['Calm', 'Stressed', 'Exhausted', 'Fulfilled', 'Depressed', 'Happy'],
      female_hair_types: ['bob', 'bun', 'curly', 'long_not_too_long', 'shaggy', 'straight_1', 'straight_2'],
      male_hair_types: ['buzzcut', 'short_flat', 'short_round', 'short_waved', 'sides'],
    };
  }
  return staticGameData;
}

export type GameStatus = 'creating' | 'playing' | 'paused' | 'dead';
export type Controller = 'active' | 'inactive' | 'paused';
export type Connection = 'connected' | 'disconnected';
export type Season = 'spring' | 'summer' | 'autumn' | 'winter';

// Conversation object interface - full type for conversations array
// Uses generic interface to avoid circular deps with conversation service
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface ConversationObjLike {
  id: string;
  character?: string | Person;  // Can be string ID, Person object, or undefined
  cType?: string | null;
  conversation: unknown[];
  question: number;
  unread: boolean;
  summary?: string;
  summary_message_count?: number;
  // Use 'any' for method signatures to avoid circular dependency type issues
  addMessage?: (...args: any[]) => void;
  getAnswerOptions?: () => string[] | undefined;
  setAnswerOptions?: (options: string[]) => void;
  getRecentMessages?: (count: number) => unknown[];
  getMessageCount?: () => number;
  toJSON?: () => Record<string, unknown>;
}

// Messaging modifiers for relationships
export interface MessagingModifiers {
  verbosity?: number;
  inquisitiveness?: number;
  expressiveness?: number;
  responsiveness?: number;
  openness?: number;
  emoji_usage?: number;
  formality?: number;
  response_timing?: number;
  mood_state?: 'great' | 'bad' | 'stressed' | 'neutral';
  current_topic_engagement?: number;
}

// Relationship data for romantic relationships (DatingView expects this shape)
export interface RelationshipData {
  id?: string;
  person1?: string;
  person2?: string;
  startDate?: string;
  anniversaryDate?: string;
  relationshipStatus?: string;
  description?: string;
  relationshipNotes?: string;
  relationshipScore?: number;
  eventsLog?: string[];
  commonInterests?: string[];
  challenges?: string[];
  futurePlans?: string[];
}

// Messaging style modifiers are stored separately from romantic relationship data.
export interface MessagingModifierData {
  characterId: string;
  messaging_modifiers?: MessagingModifiers;
}

// Welcome-back digest: a compact summary of what happened while the player was
// offline, produced by LoopManager.processOfflineTime and surfaced on
// init/reconnect (both inside playerObject.offlineStats and as a dedicated
// `offlineDigest` message). Persisted with offlineStats so it survives a
// save/load between the offline loop and the player's next connection.
export interface OfflineDigest {
  /** Real minutes the player was away (capped to the offline-processing cap). */
  minutesAway: number;
  /** Net money change accrued across the offline period. */
  moneyDelta: number;
  /** Whole years the character aged while offline. */
  ageYearsDelta: number;
  /** Up to 3 most-notable event messages that fired while offline. */
  notableEvents: string[];
  /** ISO timestamp the digest was generated. */
  generatedAt: string;
}

// Offline statistics tracking
export interface OfflineStats {
  minutesOffline: number;
  lastOnline?: Date;
  digest?: OfflineDigest;
}

// Pending conversation event types
export type PendingEventType =
  | 'activity_invite'
  | 'date_request'
  | 'emotional_moment'
  | 'favor_request'
  | 'gift_received'
  | 'confession'
  | 'relationship_upgrade'
  | 'relationship_accepted'
  | 'date_accepted'
  | 'activity_accepted'
  | 'confession_accepted'
  | 'breakup_initiated'
  | 'breakup_accepted';

// Pending conversation event from AI tool calls
export interface PendingConversationEvent {
  id: string;
  type: PendingEventType;
  characterId: string;
  characterName: string;
  data: Record<string, unknown>;
  createdAt: string;
  expiresAt?: string;
  triggersAt?: number; // Game hour when this event should trigger (for scheduled dates/activities)
  priority: 'low' | 'medium' | 'high';
}

// Tool cooldown tracking - maps characterId -> toolName -> gameHour last used
export type ToolCooldowns = Record<string, Record<string, number>>;

// Pending announcement from AI tool calls (shows as popup/notification)
export interface PendingAnnouncement {
  id: string;
  characterId: string;
  characterName: string;
  title: string;
  message: string;
  image?: string;
  category?: 'social' | 'romantic' | 'gift' | 'activity' | 'emotional';
  createdAt: string;
}

export interface LiveActivityRegistration {
  activityId: string;
  pushToken: string;
  characterId: string;
  startedAt: string;
  lastSentAt?: string;
}

// Location class for game world (matches Python locationClass)
export interface GameLocation {
  id: string;
  type: string;
  name?: string;
  image?: string;
  description?: string;
  people?: string[];  // IDs of people at this location
}

export interface PlayerData {
  userId: string;
  character: Person;
  status?: GameStatus;
  controller?: Controller;
  connection?: Connection;
  date?: string;
  time?: string;
  hourOfDay?: number;
  minuteOfHour?: number;
  dayOfYear?: number;
  dayOfWeek?: number;
  monthOfYear?: number;
  season?: Season;
  gameSpeed?: number;
  previousGameSpeed?: number;
  money?: number;
  diamonds?: number;
  relationships?: RelationshipPerson[];
  r?: Person[];  // Full person objects for relationships
  relData?: RelationshipData[];  // Romantic relationship entries only
  messagingModifiers?: MessagingModifierData[];  // Conversation messaging modifiers
  conversations?: ConversationObjLike[];  // Active conversations
  events?: Set<string> | string[];  // Set in code, array from JSON
  askedQuestions?: Set<string> | string[];  // Set in code, array from JSON
  // v2 EventEngine cooldown map: event id -> in-game day it last fired on.
  // Plain object so it round-trips through JSON natively (no Set conversion).
  eventLastFired?: Record<string, number>;
  // v2 persistent choice flags (set via a choice's setFlags) that gate
  // multi-stage arcs. Set in code, array from JSON (mirrors askedQuestions).
  flags?: Set<string> | string[];
  deviceToken?: string;
  liveActivity?: LiveActivityRegistration;
  tutorialStep?: number;
  onboardingComplete?: boolean;
  messageQueue?: string[];
  messageLog?: string[];
  lifecycleQueue?: Array<{ type: string; data?: Record<string, unknown> }>;
  activeDilemmas?: unknown[];  // Legacy v1 dilemmas (deprecated, inactive runtime path)
  // Game loop properties
  ticks?: number;
  weekDayText?: string;
  dayEvent?: string | boolean;
  summerVacation?: boolean;
  daysUntilSchoolEnds?: number;
  daysSinceSchoolStarted?: number;
  updateClient?: boolean;
  offlineStats?: OfflineStats;
  weekend?: boolean;
  weather?: 'sunny' | 'cloudy' | 'rainy' | 'snowy' | 'stormy' | 'windy' | 'foggy';
  l?: GameLocation[];  // Locations
  // AI tool calling support
  pendingConversationEvents?: PendingConversationEvent[];
  toolCooldowns?: ToolCooldowns;
  pendingAnnouncements?: PendingAnnouncement[];
  // End-of-life replay hook: populated by handleDeath, cleared by startNewLife.
  lifeSummary?: LifeSummary | null;
  // ── Generational / legacy system (T010b) ──────────────────────────────────
  // Compounding family prestige carried across generations. Persisted OUTSIDE
  // the per-life wipe — only a deliberate fresh start clears it.
  familyPrestige?: number;
  // Persistent lineage history; one entry appended per death, survives new life.
  familyTree?: FamilyTreeEntry[];
  // Inheritance amount stashed at death, applied to the heir's new life by the
  // continue-as-heir branch of startNewLife. Cleared once consumed.
  pendingInheritance?: number;
  // ── Hall of Fame (cross-life personal best) ───────────────────────────────
  // Survives EVERY new life, including a deliberate fresh start, so there is a
  // persistent "beat your best life" hook across the whole save. Never wiped.
  hallOfFame?: HallOfFame;
  // ── Life Goals / Aspirations (T011a) ─────────────────────────────────────
  // Forward-looking goal slate + completed history + running life score.
  // Persisted so the player's destination survives save/load. The authoritative
  // runtime state lives in the lifeGoals service (keyed by playerId); this blob
  // is the serialized snapshot hydrated on load and refreshed on save.
  lifeGoals?: PersistedLifeGoals;
  // ── Quest engagement (T011c) ──────────────────────────────────────────────
  // Deeper daily-return loop state: all-quests full-clear streak + the rolled
  // weekly challenge. Persisted so streak/weekly progress survives save/load.
  // (Daily quest chains are day-scoped and intentionally NOT persisted.)
  questEngagement?: PersistedQuestEngagement;
  // ── Account deletion (Apple-required functional deletion) ─────────────────
  // ISO timestamp marking when this account becomes eligible for permanent
  // purge (set to now + 30-day grace period when the player confirms deletion;
  // cleared on cancel). Absent/null => account is NOT scheduled for deletion.
  // The purge background job only deletes accounts whose deletionScheduledAt is
  // in the PAST.
  deletionScheduledAt?: string | null;
}

/** Serialized life-goals snapshot stored in the player JSON blob. */
export interface PersistedLifeGoals {
  active: Array<{ id: string; current: number; progressPercent: number }>;
  completed: Array<{ id: string; completedAt: string }>;
  lifeScore: number;
}

/** Serialized quest-engagement snapshot (streak + weekly challenge). */
export interface PersistedQuestEngagement {
  fullClearStreak: number;
  lastFullClearDate: string | null;
  lastStreakBonusDate: string | null;
  weekly: {
    id: string;
    questType: string;
    description: string;
    progress: number;
    progressRequired: number;
    diamondReward: number;
    completed: boolean;
    claimed: boolean;
    weekKey: string;
    iconName: string;
  } | null;
}

export class Player {
  userId: string;
  character: Person;
  status: GameStatus;
  controller: Controller;
  connection: Connection;
  date: string;
  time: string;
  hourOfDay: number;
  minuteOfHour: number;
  dayOfYear: number;
  dayOfWeek: number;
  monthOfYear: number;
  season: Season;
  gameSpeed: number;
  previousGameSpeed: number;
  money: number;
  diamonds: number;
  relationships: RelationshipPerson[];
  r: Person[];  // Full person objects for relationships
  relData: RelationshipData[];  // Romantic relationship entries only
  messagingModifiers: MessagingModifierData[];  // Conversation messaging modifiers
  conversations: ConversationObjLike[];  // Active conversations
  events: Set<string>;
  askedQuestions: Set<string>;
  // v2 EventEngine cooldown map (event id -> in-game day last fired). Persisted
  // so cooldowns (late-life recurring beats, performanceReview) survive restart.
  eventLastFired: Record<string, number>;
  // v2 persistent choice flags gating multi-stage arcs. Persisted (as an array
  // in JSON) so staged follow-ups remember which branch the player chose.
  flags: Set<string>;
  deviceToken: string;
  liveActivity?: LiveActivityRegistration;
  tutorialStep: number;
  onboardingComplete: boolean;
  messageQueue: string[];
  messageLog: string[];
  lifecycleQueue: Array<{ type: string; data?: Record<string, unknown> }>;
  activeDilemmas: unknown[];  // Legacy v1 dilemmas (deprecated, inactive runtime path)
  // Game loop properties
  ticks: number;
  weekDayText: string;
  dayEvent: string | boolean;
  summerVacation: boolean;
  daysUntilSchoolEnds: number;
  daysSinceSchoolStarted: number;
  updateClient: boolean;
  offlineStats: OfflineStats;
  weekend: boolean;
  weather: 'sunny' | 'cloudy' | 'rainy' | 'snowy' | 'stormy' | 'windy' | 'foggy';
  l: GameLocation[];  // Locations
  // AI tool calling support
  pendingConversationEvents: PendingConversationEvent[];
  toolCooldowns: ToolCooldowns;
  pendingAnnouncements: PendingAnnouncement[];
  // End-of-life replay hook: set on death, cleared on New Life.
  lifeSummary: LifeSummary | null;
  // Generational / legacy system: persisted across the per-life wipe.
  familyPrestige: number;
  familyTree: FamilyTreeEntry[];
  pendingInheritance: number;
  // Cross-life personal best — survives every new life (fresh or heir).
  hallOfFame: HallOfFame;
  // Life Goals snapshot (persisted destination state). May be undefined on
  // older saves / fresh players; the lifeGoals service seeds it on first use.
  lifeGoals?: PersistedLifeGoals;
  // Quest engagement snapshot (full-clear streak + weekly challenge). May be
  // undefined on older saves; the dailyQuests service seeds it on first use.
  questEngagement?: PersistedQuestEngagement;
  // Account deletion grace-period marker (ISO timestamp). When set in the PAST,
  // the purge background job permanently deletes this account. null/undefined =
  // not scheduled for deletion.
  deletionScheduledAt?: string | null;
  // Flag to indicate data was migrated and needs saving (e.g., avatar URLs fixed)
  _needsSave: boolean = false;

  // Alias for Python compatibility (c = character)
  get c(): Person {
    return this.character;
  }
  set c(value: Person) {
    this.character = value;
  }

  constructor(data: PlayerData) {
    this.userId = data.userId;
    // Handle character being either a Person instance or plain object from JSON
    // Also handle 'c' alias from Python format
    const charData = data.character ?? (data as any).c;
    this.character = charData instanceof Person
      ? charData
      : new Person(charData as any);
    this.status = data.status ?? 'creating';
    this.controller = data.controller ?? 'inactive';
    this.connection = data.connection ?? 'connected';
    this.date = data.date ?? new Date().toISOString().split('T')[0];
    this.time = data.time ?? '08:00';
    this.hourOfDay = data.hourOfDay ?? 8;
    this.minuteOfHour = data.minuteOfHour ?? 0;
    this.dayOfYear = data.dayOfYear ?? 1;
    this.dayOfWeek = data.dayOfWeek ?? 1;
    this.monthOfYear = data.monthOfYear ?? 1;
    this.season = data.season ?? 'spring';
    this.gameSpeed = data.gameSpeed ?? 10000;
    this.previousGameSpeed = data.previousGameSpeed ?? 10000;
    this.money = data.money ?? 0;
    this.diamonds = data.diamonds ?? 0;
    this.relationships = data.relationships ?? [];
    // Handle r being plain objects from JSON - convert to Person instances
    this.r = (data.r ?? []).map(p => p instanceof Person ? p : new Person(p as any));

    const isMessagingModifierEntry = (entry: unknown): entry is MessagingModifierData => {
      if (!entry || typeof entry !== 'object') return false;
      const data = entry as Record<string, unknown>;
      return typeof data.characterId === 'string';
    };

    const isRelationshipEntry = (entry: unknown): entry is RelationshipData => {
      if (!entry || typeof entry !== 'object') return false;
      const data = entry as Record<string, unknown>;
      return (
        typeof data.person1 === 'string' ||
        typeof data.person2 === 'string' ||
        typeof data.relationshipStatus === 'string' ||
        (typeof data.id === 'string' && Array.isArray(data.eventsLog))
      );
    };

    const rawRelData = Array.isArray(data.relData) ? data.relData : [];
    const relationshipEntries: RelationshipData[] = [];
    const messagingEntries = new Map<string, MessagingModifierData>();
    let migratedLegacyMessaging = false;

    for (const entry of Array.isArray(data.messagingModifiers) ? data.messagingModifiers : []) {
      if (isMessagingModifierEntry(entry)) {
        messagingEntries.set(entry.characterId, entry);
      }
    }

    for (const entry of rawRelData) {
      if (isMessagingModifierEntry(entry)) {
        migratedLegacyMessaging = true;
        if (!messagingEntries.has(entry.characterId)) {
          messagingEntries.set(entry.characterId, entry);
        }
        continue;
      }

      if (isRelationshipEntry(entry)) {
        relationshipEntries.push(entry);
      }
    }

    this.relData = relationshipEntries;
    this.messagingModifiers = Array.from(messagingEntries.values());

    // Ensure all persons have valid avatar images
    // Regenerate if missing OR if using old broken clothing values from DiceBear API
    const needsAvatarRegeneration = (image: string): boolean => {
      if (!image) return true;
      // Migrate any legacy DiceBear-hosted avatar onto the curated library.
      if (image.includes('api.dicebear.com')) return true;
      // Old invalid clothing values that caused 400 errors on DiceBear
      const brokenPatterns = [
        /&clothing=blazer&/,      // Should be blazerAndShirt or blazerAndSweater
        /&clothing=sweater&/,     // Should be collarAndSweater or blazerAndSweater
        /&clothing=shirt&/,       // Should be shirtCrewNeck, shirtScoopNeck, shirtVNeck
      ];
      return brokenPatterns.some(pattern => pattern.test(image));
    };

    let avatarsRegenerated = false;
    if (this.character && needsAvatarRegeneration(this.character.image)) {
      setAvatar(this.character);
      avatarsRegenerated = true;
    }
    for (const person of this.r) {
      if (needsAvatarRegeneration(person.image)) {
        setAvatar(person);
        avatarsRegenerated = true;
      }
    }
    // Flag to indicate data migration happened and should be saved
    this._needsSave = avatarsRegenerated || migratedLegacyMessaging || relationshipEntries.length !== rawRelData.length;
    this.conversations = data.conversations ?? [];
    // Handle events/askedQuestions being either Sets or arrays (from JSON)
    // JSON.parse() turns Sets into arrays, so we need to convert back
    this.events = data.events instanceof Set
      ? data.events
      : new Set(Array.isArray(data.events) ? data.events : []);
    this.askedQuestions = data.askedQuestions instanceof Set
      ? data.askedQuestions
      : new Set(Array.isArray(data.askedQuestions) ? data.askedQuestions : []);
    // Carry-forward fix (T011b): hydrate the v2 cooldown map + choice flags so
    // they survive save/load. eventLastFired is a plain object; flags mirrors
    // askedQuestions (Set in code, array from JSON).
    this.eventLastFired =
      data.eventLastFired && typeof data.eventLastFired === 'object'
        ? { ...data.eventLastFired }
        : {};
    this.flags = data.flags instanceof Set
      ? data.flags
      : new Set(Array.isArray(data.flags) ? data.flags : []);
    this.deviceToken = data.deviceToken ?? '';
    this.liveActivity = data.liveActivity;
    this.tutorialStep = data.tutorialStep ?? 0;
    this.onboardingComplete = data.onboardingComplete ?? false;
    this.messageQueue = data.messageQueue ?? [];
    this.messageLog = data.messageLog ?? [];
    this.lifecycleQueue = [];  // Transient — never persisted
    // Legacy compatibility field; active runtime uses event_instances persistence.
    this.activeDilemmas = data.activeDilemmas ?? [];
    // Game loop properties
    this.ticks = data.ticks ?? 0;
    this.weekDayText = data.weekDayText ?? 'Monday';
    this.dayEvent = data.dayEvent ?? false;
    this.summerVacation = data.summerVacation ?? false;
    this.daysUntilSchoolEnds = data.daysUntilSchoolEnds ?? 0;
    this.daysSinceSchoolStarted = data.daysSinceSchoolStarted ?? 0;
    this.updateClient = data.updateClient ?? false;
    this.offlineStats = data.offlineStats ?? { minutesOffline: 0 };
    this.weekend = data.weekend ?? false;
    this.weather = data.weather ?? 'sunny';
    this.l = data.l ?? [];
    // AI tool calling support
    this.pendingConversationEvents = data.pendingConversationEvents ?? [];
    this.toolCooldowns = data.toolCooldowns ?? {};
    this.pendingAnnouncements = data.pendingAnnouncements ?? [];
    this.lifeSummary = data.lifeSummary ?? null;
    // Generational / legacy system.
    this.familyPrestige = data.familyPrestige ?? 0;
    this.familyTree = Array.isArray(data.familyTree) ? data.familyTree : [];
    this.pendingInheritance = data.pendingInheritance ?? 0;
    this.hallOfFame = data.hallOfFame ?? { bestScore: 0, lives: 0, recentScores: [], bestLife: null };
    this.lifeGoals = data.lifeGoals;
    this.questEngagement = data.questEngagement;
    this.deletionScheduledAt = data.deletionScheduledAt ?? null;
  }

  get isActive(): boolean {
    return this.controller === 'active' && this.status === 'playing';
  }

  getTicksForSpeed(): number {
    return this.gameSpeed;
  }

  toJSON(): Record<string, unknown> {
    return {
      // Type identifier like Python
      type: 'playerObject',
      userId: this.userId,
      c: this.character.toJSON(),
      status: this.status,
      controller: this.controller,
      connection: this.connection,
      date: this.date,
      time: this.time,
      hourOfDay: this.hourOfDay,
      minuteOfHour: this.minuteOfHour,
      dayOfYear: this.dayOfYear,
      dayOfWeek: this.dayOfWeek,
      monthOfYear: this.monthOfYear,
      season: this.season,
      gameSpeed: this.gameSpeed,
      previousGameSpeed: this.previousGameSpeed,
      money: this.money,
      diamonds: this.diamonds,
      relationships: this.relationships,
      r: this.r.map(p => p.toJSON()),
      relData: this.relData.map((rel) => ({
        id: rel.id ?? '',
        person1: rel.person1 ?? '',
        person2: rel.person2 ?? '',
        startDate: rel.startDate ?? '',
        anniversaryDate: rel.anniversaryDate ?? rel.startDate ?? '',
        relationshipStatus: rel.relationshipStatus ?? '',
        description: rel.description ?? '',
        relationshipNotes: rel.relationshipNotes ?? '',
        relationshipScore: rel.relationshipScore ?? 0,
        eventsLog: Array.isArray(rel.eventsLog) ? rel.eventsLog : [],
        commonInterests: Array.isArray(rel.commonInterests) ? rel.commonInterests : [],
        challenges: Array.isArray(rel.challenges) ? rel.challenges : [],
        futurePlans: Array.isArray(rel.futurePlans) ? rel.futurePlans : [],
      })),
      messagingModifiers: this.messagingModifiers,
      conversations: this.conversations.map(c => ({
        id: c.id,
        type: 'conversationEvent',
        character: c.character,
        cType: c.cType,
        conversation: c.conversation,
        question: c.question,
        unread: c.unread,
        summary: c.summary,
        summary_message_count: c.summary_message_count,
      })),
      // CRITICAL: Convert Sets to arrays for JSON serialization
      // Sets serialize as {} which breaks save/load
      events: Array.from(this.events),
      askedQuestions: Array.from(this.askedQuestions),
      // Carry-forward fix (T011b): persist v2 cooldown map + choice flags.
      eventLastFired: { ...this.eventLastFired },
      flags: Array.from(this.flags),
      deviceToken: this.deviceToken,
      liveActivity: this.liveActivity,
      tutorialStep: this.tutorialStep,
      onboardingComplete: this.onboardingComplete,
      messageQueue: this.messageQueue,
      messageLog: this.messageLog,
      activeDilemmas: this.activeDilemmas,
      // Game loop properties
      ticks: this.ticks,
      weekDayText: this.weekDayText,
      dayEvent: this.dayEvent,
      summerVacation: this.summerVacation,
      daysUntilSchoolEnds: this.daysUntilSchoolEnds,
      daysSinceSchoolStarted: this.daysSinceSchoolStarted,
      updateClient: this.updateClient,
      offlineStats: this.offlineStats,
      weekend: this.weekend,
      weather: this.weather,
      l: this.l,
      // AI tool calling support
      pendingConversationEvents: this.pendingConversationEvents,
      toolCooldowns: this.toolCooldowns,
      pendingAnnouncements: this.pendingAnnouncements,
      lifeSummary: this.lifeSummary,
      // Generational / legacy system.
      familyPrestige: this.familyPrestige,
      familyTree: this.familyTree,
      pendingInheritance: this.pendingInheritance,
      hallOfFame: this.hallOfFame,
      // Life Goals snapshot (forward-looking aspirations). Embed the FORMATTED
      // goals (with title/description/icon/target/reward) so the cached
      // playerObject matches the `lifeGoalsUpdate` shape — otherwise iOS caches
      // empty titles on initial connect and renders blank goal cells.
      lifeGoals: {
        active: formatActiveGoals(this.userId),
        completed: formatCompletedGoals(this.userId),
        lifeScore: getLifeScore(this.userId),
      },
      // Quest engagement snapshot (full-clear streak + weekly challenge).
      questEngagement: this.questEngagement,
      // Account deletion grace-period marker (persisted so the scheduled
      // deletion survives save/load and the purge job can find it).
      deletionScheduledAt: this.deletionScheduledAt ?? null,
      // Static game data - like Python includes in playerClass
      ...getStaticGameData(),
    };
  }
}
