/**
 * Daily Quest System
 *
 * Generates and tracks daily quests for player engagement.
 * Players receive 3 quests daily (easy/medium/hard) that award diamonds on completion.
 *
 * Database Tables:
 * - daily_quest_templates: Quest definitions (type, description, requirements, rewards)
 * - player_daily_quests: Player's assigned quests with progress tracking
 */

import { query, queryOne, execute, getConnection } from '../../database/index.js';
import { awardDiamonds } from '../../monetization/diamondEconomy.js';
import type { RowDataPacket, ResultSetHeader, PoolConnection } from 'mysql2/promise';

// ============================================
// Type Definitions
// ============================================

export type QuestType =
  | 'talk_to_characters'
  | 'buy_item'
  | 'attend_class'
  | 'socialize'
  | 'work_hours'
  | 'go_on_date'
  | 'complete_activities'
  | 'study'
  | 'spend_energy'
  | 'earn_money'
  | 'increase_affinity';

export type QuestDifficulty = 'easy' | 'medium' | 'hard';

export interface QuestTemplate {
  type: QuestType;
  desc: string;
  required: number;
  reward: number;
  difficulty: QuestDifficulty;
  energy: number;
  icon: string;
}

export interface ActiveQuest {
  id: string;
  questType: QuestType;
  description: string;
  progress: number;
  progressRequired: number;
  diamondReward: number;
  difficulty: QuestDifficulty;
  iconName: string;
  completed: boolean;
  completedDate?: string;
  justCompleted?: boolean;
}

export interface QuestReward {
  diamonds: number;
  energy: number | null;
  money: number | null;
}

export interface FormattedQuest {
  id: string;
  name: string;
  description: string;
  category: string;
  reward: QuestReward;
  progress: number;
  target: number;
  completed: boolean;
  claimed: boolean;
}

export interface QuestStatistics {
  totalCompleted: number;
  totalDiamondsEarned: number;
  daysWithQuests: number;
  byDifficulty: Record<string, number>;
}

export interface ClaimQuestResult {
  success: boolean;
  message: string;
  reward: QuestReward | null;
}

interface DiamondAwardPlayer {
  userId?: string;
  c?: {
    diamonds?: number;
  };
  character?: {
    diamonds?: number;
  };
}

// Database row interfaces
interface QuestTemplateRow extends RowDataPacket {
  id: number;
  quest_type: QuestType;
  description: string;
  progress_required: number;
  diamond_reward: number;
  difficulty: QuestDifficulty;
  energy_cost: number;
  icon_name: string;
}

interface PlayerQuestRow extends RowDataPacket {
  id: number;
  player_id: string;
  quest_id: number;
  quest_date: Date;
  progress: number;
  completed: number;
  completed_at: Date | null;
  quest_type: QuestType;
  description: string;
  progress_required: number;
  diamond_reward: number;
  difficulty: QuestDifficulty;
  icon_name: string;
}

interface CountRow extends RowDataPacket {
  count: number;
}

interface StatsRow extends RowDataPacket {
  total_completed: number;
  total_diamonds_earned: number | null;
  days_completed: number;
}

interface DifficultyCountRow extends RowDataPacket {
  difficulty: QuestDifficulty;
  completed_count: number;
}

interface ColumnNameRow extends RowDataPacket {
  COLUMN_NAME: string;
}

interface IndexCountRow extends RowDataPacket {
  count: number;
}

// ============================================
// Quest Template Definitions
// ============================================

const QUEST_TEMPLATES: QuestTemplate[] = [
  // Easy quests (10 energy, quick)
  { type: 'talk_to_characters', desc: 'Talk to 3 different characters', required: 3,
    reward: 10, difficulty: 'easy', energy: 9, icon: 'message' },
  { type: 'buy_item', desc: 'Buy an item from the store', required: 1,
    reward: 5, difficulty: 'easy', energy: 0, icon: 'cart' },
  // NOTE: 'attend_class' was pruned — school is a passive dailyPlan location
  // processed per-person in both loops with no per-class-attended event and no
  // single-fire site, so the quest was permanently uncompletable. The 'study'
  // quest already covers the education loop via the player-action path.
  { type: 'socialize', desc: 'Socialize with friends', required: 2,
    reward: 10, difficulty: 'easy', energy: 6, icon: 'person.2' },

  // Medium quests (20-30 energy)
  // NOTE: 'work_hours' was pruned — work is a passive dailyPlan location with no
  // per-shift "hours worked" event and no honest single-fire source (the only
  // clean weekly number is gross dollars, which the 'earn_money' quest tracks).
  { type: 'go_on_date', desc: 'Go on a date', required: 1,
    reward: 20, difficulty: 'medium', energy: 15, icon: 'heart' },
  { type: 'complete_activities', desc: 'Complete 5 activities', required: 5,
    reward: 18, difficulty: 'medium', energy: 25, icon: 'checkmark.circle' },
  { type: 'study', desc: 'Study for 4 hours', required: 4,
    reward: 15, difficulty: 'medium', energy: 12, icon: 'book.fill' },

  // Hard quests (40+ energy)
  { type: 'spend_energy', desc: 'Spend 50 energy on activities', required: 50,
    reward: 25, difficulty: 'hard', energy: 50, icon: 'bolt' },
  { type: 'earn_money', desc: 'Earn $500', required: 500,
    reward: 30, difficulty: 'hard', energy: 40, icon: 'dollarsign' },
  { type: 'increase_affinity', desc: 'Increase affinity by 20 points total', required: 20,
    reward: 35, difficulty: 'hard', energy: 45, icon: 'heart.fill' },
];

// ============================================
// Quest Chains
// ============================================

/**
 * Quest chain definitions: completing a base quest type unlocks a follow-up
 * "bonus" quest (a deeper version of the same loop) that the player can also
 * complete that day for an additional diamond reward. Chains deepen the daily
 * loop without inflating the base 3-quest slate.
 */
export interface QuestChainStep {
  type: QuestType;
  desc: string;
  required: number;
  reward: number;
  difficulty: QuestDifficulty;
  icon: string;
}

const QUEST_CHAINS: Record<string, QuestChainStep> = {
  talk_to_characters: {
    type: 'talk_to_characters',
    desc: 'Chain: Talk to 5 more characters',
    required: 5,
    reward: 15,
    difficulty: 'medium',
    icon: 'message.badge',
  },
  // NOTE: 'work_hours' chain removed alongside the pruned work_hours base quest.
  earn_money: {
    type: 'earn_money',
    desc: 'Chain: Earn $1000 more',
    required: 1000,
    reward: 40,
    difficulty: 'hard',
    icon: 'dollarsign.circle.fill',
  },
};

export function getQuestChainStep(baseType: QuestType): QuestChainStep | undefined {
  return QUEST_CHAINS[baseType];
}

// ============================================
// Weekly Challenge Quests
// ============================================

/**
 * Weekly challenge quests: bigger, week-long objectives with substantially
 * larger diamond payouts than dailies. One challenge is rolled per ISO week and
 * persists/accumulates progress across the week. Kept in a separate catalog so
 * the daily QUEST_TEMPLATES list (and its tests) are unaffected.
 */
export interface WeeklyChallengeTemplate {
  id: string;
  type: QuestType;
  desc: string;
  required: number;
  reward: number;
  icon: string;
}

const WEEKLY_CHALLENGE_TEMPLATES: WeeklyChallengeTemplate[] = [
  { id: 'weekly_earn', type: 'earn_money', desc: 'Weekly Challenge: Earn $5000', required: 5000, reward: 100, icon: 'dollarsign.circle.fill' },
  // NOTE: 'weekly_work' (work_hours) removed — work_hours has no completion path.
  { id: 'weekly_social', type: 'talk_to_characters', desc: 'Weekly Challenge: Talk to 25 characters', required: 25, reward: 90, icon: 'person.3.fill' },
  { id: 'weekly_activities', type: 'complete_activities', desc: 'Weekly Challenge: Complete 30 activities', required: 30, reward: 110, icon: 'checkmark.seal.fill' },
];

export function getWeeklyChallengeTemplates(): WeeklyChallengeTemplate[] {
  return [...WEEKLY_CHALLENGE_TEMPLATES];
}

/** ISO-ish week key (year + week number) used to roll/scope weekly challenges. */
export function getWeekKey(date: Date = new Date()): string {
  const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  const dayNum = (d.getUTCDay() + 6) % 7; // Mon=0
  d.setUTCDate(d.getUTCDate() - dayNum + 3); // nearest Thursday
  const firstThursday = new Date(Date.UTC(d.getUTCFullYear(), 0, 4));
  const week = 1 + Math.round(
    ((d.getTime() - firstThursday.getTime()) / 86400000 - 3 + ((firstThursday.getUTCDay() + 6) % 7)) / 7
  );
  return `${d.getUTCFullYear()}-W${String(week).padStart(2, '0')}`;
}

// ============================================
// All-Quests Streak Bonus
// ============================================

/** Consecutive full-clear days required to fire the streak bonus. */
export const STREAK_BONUS_THRESHOLD = 3;
/** Diamonds awarded when the streak bonus fires. */
export const STREAK_BONUS_REWARD = 50;

export interface ActiveWeeklyChallenge {
  id: string;
  questType: QuestType;
  description: string;
  progress: number;
  progressRequired: number;
  diamondReward: number;
  completed: boolean;
  claimed: boolean;
  weekKey: string;
  iconName: string;
}

export interface StreakBonusResult {
  fired: boolean;
  streakDays: number;
  diamonds: number;
}

/**
 * Per-player engagement state for the deeper quest loop: full-clear streak,
 * the unlocked-chain step ids for the current day, and the rolled weekly
 * challenge. Held in memory (authoritative) and snapshotted into the player
 * blob via serializeQuestEngagement (mirrors the lifeGoals persistence model).
 */
interface QuestEngagementState {
  // All-quests streak
  fullClearStreak: number;
  lastFullClearDate: string | null;
  lastStreakBonusDate: string | null;
  // Chains unlocked today: base quest type -> chain step record
  chainDate: string | null;
  chains: Record<string, { progress: number; completed: boolean; claimed: boolean }>;
  // Weekly challenge
  weekly: ActiveWeeklyChallenge | null;
}

const questEngagement: Map<string, QuestEngagementState> = new Map();

function getEngagement(playerId: string): QuestEngagementState {
  let state = questEngagement.get(playerId);
  if (!state) {
    state = {
      fullClearStreak: 0,
      lastFullClearDate: null,
      lastStreakBonusDate: null,
      chainDate: null,
      chains: {},
      weekly: null,
    };
    questEngagement.set(playerId, state);
  }
  return state;
}

// ============================================
// In-Memory Storage (Fallback)
// ============================================

interface PlayerQuestRecord {
  id: string;
  templateType: QuestType;
  assignedDate: Date;
  progress: number;
  completed: boolean;
  completedDate?: Date;
  claimed: boolean;
}

const playerQuests: Map<string, PlayerQuestRecord[]> = new Map();
let questIdCounter = 0;

// Configuration flag - set to true to use database
let useDatabaseStorage = false;

/**
 * Enable database storage for quests
 */
export function enableDatabaseStorage(): void {
  useDatabaseStorage = true;
}

/**
 * Disable database storage (use in-memory fallback)
 */
export function disableDatabaseStorage(): void {
  useDatabaseStorage = false;
}

// ============================================
// Database Initialization
// ============================================

async function getTableColumns(connection: PoolConnection, tableName: string): Promise<Set<string>> {
  const [rows] = await connection.execute<ColumnNameRow[]>(
    `SELECT COLUMN_NAME
     FROM information_schema.columns
     WHERE table_schema = DATABASE()
       AND table_name = ?`,
    [tableName]
  );

  return new Set(rows.map(row => row.COLUMN_NAME.toLowerCase()));
}

async function hasIndex(
  connection: PoolConnection,
  tableName: string,
  indexName: string
): Promise<boolean> {
  const [rows] = await connection.execute<IndexCountRow[]>(
    `SELECT COUNT(*) AS count
     FROM information_schema.statistics
     WHERE table_schema = DATABASE()
       AND table_name = ?
       AND index_name = ?`,
    [tableName, indexName]
  );

  return (rows[0]?.count ?? 0) > 0;
}

/**
 * Ensure daily quest tables exist and normalize legacy column names.
 */
export async function ensureDailyQuestTables(): Promise<void> {
  const connection = await getConnection();
  try {
    await connection.execute(`
      CREATE TABLE IF NOT EXISTS daily_quest_templates (
        id INT AUTO_INCREMENT PRIMARY KEY,
        quest_type VARCHAR(100) NOT NULL,
        description TEXT NOT NULL,
        progress_required INT NOT NULL,
        diamond_reward INT NOT NULL DEFAULT 0,
        difficulty ENUM('easy', 'medium', 'hard') NOT NULL DEFAULT 'easy',
        energy_cost INT NOT NULL DEFAULT 0,
        icon_name VARCHAR(100) NOT NULL DEFAULT 'star',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    `);

    // De-duplicate any legacy rows before applying unique index.
    await connection.execute(`
      DELETE t1 FROM daily_quest_templates t1
      INNER JOIN daily_quest_templates t2
        ON t1.quest_type = t2.quest_type
       AND t1.id > t2.id
    `);

    if (!(await hasIndex(connection, 'daily_quest_templates', 'uq_daily_quest_templates_quest_type'))) {
      await connection.execute(`
        ALTER TABLE daily_quest_templates
        ADD UNIQUE KEY uq_daily_quest_templates_quest_type (quest_type)
      `);
    }

    await connection.execute(`
      CREATE TABLE IF NOT EXISTS player_daily_quests (
        id BIGINT AUTO_INCREMENT PRIMARY KEY,
        player_id VARCHAR(128) NOT NULL,
        quest_id INT NOT NULL,
        quest_date DATE NOT NULL,
        progress INT DEFAULT 0,
        completed BOOLEAN DEFAULT FALSE,
        completed_at DATETIME NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        UNIQUE KEY uq_player_daily_quest_date (player_id, quest_id, quest_date),
        INDEX idx_player_date (player_id, quest_date),
        INDEX idx_completed (completed),
        INDEX idx_quest (quest_id)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    `);

    const playerQuestColumns = await getTableColumns(connection, 'player_daily_quests');

    if (!playerQuestColumns.has('quest_id')) {
      await connection.execute('ALTER TABLE player_daily_quests ADD COLUMN quest_id INT NULL');
      if (playerQuestColumns.has('quest_template_id')) {
        await connection.execute(`
          UPDATE player_daily_quests
          SET quest_id = quest_template_id
          WHERE quest_id IS NULL
        `);
      }
    }

    if (!playerQuestColumns.has('quest_date')) {
      await connection.execute('ALTER TABLE player_daily_quests ADD COLUMN quest_date DATE NULL');
      if (playerQuestColumns.has('assigned_date')) {
        await connection.execute(`
          UPDATE player_daily_quests
          SET quest_date = assigned_date
          WHERE quest_date IS NULL
        `);
      } else if (playerQuestColumns.has('created_at')) {
        await connection.execute(`
          UPDATE player_daily_quests
          SET quest_date = DATE(created_at)
          WHERE quest_date IS NULL
        `);
      } else {
        await connection.execute(`
          UPDATE player_daily_quests
          SET quest_date = CURDATE()
          WHERE quest_date IS NULL
        `);
      }
    }

    if (!playerQuestColumns.has('completed_at')) {
      await connection.execute('ALTER TABLE player_daily_quests ADD COLUMN completed_at DATETIME NULL');
      if (playerQuestColumns.has('completed_date')) {
        await connection.execute(`
          UPDATE player_daily_quests
          SET completed_at = completed_date
          WHERE completed_at IS NULL
        `);
      }
    }

    if (!playerQuestColumns.has('progress')) {
      await connection.execute('ALTER TABLE player_daily_quests ADD COLUMN progress INT DEFAULT 0');
    }

    if (playerQuestColumns.has('progress_required')) {
      await connection.execute(`
        UPDATE player_daily_quests
        SET progress_required = 0
        WHERE progress_required IS NULL
      `);
      await connection.execute(
        'ALTER TABLE player_daily_quests MODIFY COLUMN progress_required INT NOT NULL DEFAULT 0'
      );
    }

    if (!playerQuestColumns.has('completed')) {
      await connection.execute('ALTER TABLE player_daily_quests ADD COLUMN completed BOOLEAN DEFAULT FALSE');
    }

    if (playerQuestColumns.has('claimed')) {
      await connection.execute(`
        UPDATE player_daily_quests
        SET claimed = FALSE
        WHERE claimed IS NULL
      `);
      await connection.execute(
        'ALTER TABLE player_daily_quests MODIFY COLUMN claimed BOOLEAN NOT NULL DEFAULT FALSE'
      );
    }

    if (!(await hasIndex(connection, 'player_daily_quests', 'idx_player_date'))) {
      await connection.execute('ALTER TABLE player_daily_quests ADD INDEX idx_player_date (player_id, quest_date)');
    }

    if (!(await hasIndex(connection, 'player_daily_quests', 'idx_quest'))) {
      await connection.execute('ALTER TABLE player_daily_quests ADD INDEX idx_quest (quest_id)');
    }

    if (!(await hasIndex(connection, 'player_daily_quests', 'idx_completed'))) {
      await connection.execute('ALTER TABLE player_daily_quests ADD INDEX idx_completed (completed)');
    }

    if (!(await hasIndex(connection, 'player_daily_quests', 'uq_player_daily_quest_date'))) {
      try {
        await connection.execute(`
          ALTER TABLE player_daily_quests
          ADD UNIQUE KEY uq_player_daily_quest_date (player_id, quest_id, quest_date)
        `);
      } catch (error) {
        console.warn('Could not add uq_player_daily_quest_date unique key:', error);
      }
    }

    console.log('Daily quest tables verified');
  } finally {
    connection.release();
  }
}

/**
 * Initialize quest templates in database.
 * Called during server startup.
 */
export async function initializeQuestTemplates(): Promise<void> {
  try {
    await ensureDailyQuestTables();

    for (const quest of QUEST_TEMPLATES) {
      await execute(
        `INSERT INTO daily_quest_templates
         (quest_type, description, progress_required, diamond_reward, difficulty, energy_cost, icon_name)
         VALUES (?, ?, ?, ?, ?, ?, ?)
         ON DUPLICATE KEY UPDATE
         description = VALUES(description),
         progress_required = VALUES(progress_required),
         diamond_reward = VALUES(diamond_reward),
         energy_cost = VALUES(energy_cost),
         icon_name = VALUES(icon_name)`,
        [quest.type, quest.desc, quest.required, quest.reward, quest.difficulty, quest.energy, quest.icon]
      );
    }

    console.log(`Initialized ${QUEST_TEMPLATES.length} daily quest templates`);
    useDatabaseStorage = true;
  } catch (error) {
    console.error('Error initializing quest templates:', error);
    console.log('Falling back to in-memory quest storage');
    useDatabaseStorage = false;
  }
}

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

/**
 * Get quest template by type
 */
export function getQuestTemplate(type: QuestType): QuestTemplate | undefined {
  return QUEST_TEMPLATES.find(t => t.type === type);
}

/**
 * Generate unique quest ID (in-memory)
 */
function generateQuestId(): string {
  return `quest_${++questIdCounter}_${Date.now()}`;
}

/**
 * Get today's date as a string key
 */
function getTodayKey(): string {
  return new Date().toISOString().split('T')[0];
}

/**
 * Get today as a Date object (midnight)
 */
function getToday(): Date {
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  return today;
}

/**
 * Run quest operation with automatic fallback to in-memory storage.
 * If any database operation throws, disable DB mode for quests and retry in-memory.
 */
async function runWithStorageFallback<T>(
  operation: string,
  databaseOperation: () => Promise<T>,
  memoryOperation: () => T | Promise<T>
): Promise<T> {
  if (!useDatabaseStorage) {
    return await memoryOperation();
  }

  try {
    return await databaseOperation();
  } catch (error) {
    console.error(`Daily quest DB operation failed (${operation}):`, error);
    console.warn('Disabling daily quest database storage and falling back to in-memory mode');
    useDatabaseStorage = false;
    return await memoryOperation();
  }
}

// ============================================
// Database Operations
// ============================================

/**
 * Check if player has quests for today (database)
 */
async function hasQuestsTodayDb(playerId: string): Promise<boolean> {
  const today = getTodayKey();
  const result = await queryOne<CountRow>(
    'SELECT COUNT(*) as count FROM player_daily_quests WHERE player_id = ? AND quest_date = ?',
    [playerId, today]
  );
  return result !== null && result.count > 0;
}

/**
 * Get all quest templates from database by difficulty
 */
async function getQuestTemplatesFromDb(): Promise<QuestTemplateRow[]> {
  return query<QuestTemplateRow[]>('SELECT * FROM daily_quest_templates');
}

/**
 * Assign quests to player in database
 */
async function assignQuestsToDb(
  playerId: string,
  selectedTemplates: QuestTemplateRow[]
): Promise<ActiveQuest[]> {
  const today = getTodayKey();
  const assignedQuests: ActiveQuest[] = [];

  const connection = await getConnection();
  try {
    await connection.beginTransaction();

    for (const template of selectedTemplates) {
      const [result] = await connection.execute<ResultSetHeader>(
        `INSERT INTO player_daily_quests
         (player_id, quest_id, quest_date, progress, completed)
         VALUES (?, ?, ?, 0, FALSE)`,
        [playerId, template.id, today]
      );

      assignedQuests.push({
        id: String(result.insertId),
        questType: template.quest_type,
        description: template.description,
        progress: 0,
        progressRequired: template.progress_required,
        diamondReward: template.diamond_reward,
        difficulty: template.difficulty,
        iconName: template.icon_name,
        completed: false,
      });
    }

    await connection.commit();
    return assignedQuests;
  } catch (error) {
    await connection.rollback();
    throw error;
  } finally {
    connection.release();
  }
}

/**
 * Get active quests from database
 */
async function getActiveQuestsFromDb(playerId: string): Promise<ActiveQuest[]> {
  const today = getTodayKey();

  const quests = await query<PlayerQuestRow[]>(
    `SELECT pq.id, pq.progress, pq.completed, pq.completed_at,
            qt.quest_type, qt.description, qt.progress_required,
            qt.diamond_reward, qt.difficulty, qt.icon_name
     FROM player_daily_quests pq
     JOIN daily_quest_templates qt ON pq.quest_id = qt.id
     WHERE pq.player_id = ? AND pq.quest_date = ?
     ORDER BY qt.difficulty DESC`,
    [playerId, today]
  );

  return quests.map(quest => ({
    id: String(quest.id),
    questType: quest.quest_type,
    description: quest.description,
    progress: quest.progress,
    progressRequired: quest.progress_required,
    diamondReward: quest.diamond_reward,
    difficulty: quest.difficulty,
    iconName: quest.icon_name,
    completed: Boolean(quest.completed),
    completedDate: quest.completed_at?.toISOString(),
  }));
}

/**
 * Find active quest by type from database
 */
async function findActiveQuestByTypeDb(
  playerId: string,
  questType: QuestType
): Promise<PlayerQuestRow | null> {
  const today = getTodayKey();

  return queryOne<PlayerQuestRow>(
    `SELECT pq.*, qt.quest_type, qt.description, qt.progress_required, qt.diamond_reward, qt.difficulty, qt.icon_name
     FROM player_daily_quests pq
     JOIN daily_quest_templates qt ON pq.quest_id = qt.id
     WHERE pq.player_id = ? AND pq.quest_date = ?
     AND pq.completed = FALSE AND qt.quest_type = ?`,
    [playerId, today, questType]
  );
}

/**
 * Update quest progress in database
 */
async function updateQuestProgressDb(questId: number, newProgress: number): Promise<void> {
  await execute(
    'UPDATE player_daily_quests SET progress = ? WHERE id = ?',
    [newProgress, questId]
  );
}

/**
 * Mark quest as completed in database
 */
async function markQuestCompletedDb(questId: number, playerId: string): Promise<void> {
  await execute(
    `UPDATE player_daily_quests
     SET completed = TRUE
     WHERE id = ? AND player_id = ?`,
    [questId, playerId]
  );
}

/**
 * Get quest for claiming from database
 */
async function getQuestForClaimDb(questId: number, playerId: string): Promise<PlayerQuestRow | null> {
  return queryOne<PlayerQuestRow>(
    `SELECT pq.*, qt.diamond_reward
     FROM player_daily_quests pq
     JOIN daily_quest_templates qt ON pq.quest_id = qt.id
     WHERE pq.id = ? AND pq.player_id = ?`,
    [questId, playerId]
  );
}

/**
 * Set quest completed_at (for claiming)
 */
async function setQuestClaimedDb(questId: number, playerId: string): Promise<void> {
  await execute(
    `UPDATE player_daily_quests
     SET completed_at = NOW()
     WHERE id = ? AND player_id = ?`,
    [questId, playerId]
  );
}

// ============================================
// In-Memory Operations
// ============================================

/**
 * Check if player has quests for today (in-memory)
 */
function hasQuestsTodayMemory(playerId: string): boolean {
  const quests = playerQuests.get(playerId);
  if (!quests) return false;

  const todayKey = getTodayKey();
  return quests.some(q => q.assignedDate.toISOString().split('T')[0] === todayKey);
}

// ============================================
// Public API Functions
// ============================================

/**
 * Assign 3 random quests to player each day (1 easy, 1 medium, 1 hard).
 * Called at midnight or on first login of the day.
 */
export async function generateDailyQuests(playerId: string): Promise<ActiveQuest[]> {
  return runWithStorageFallback(
    `generateDailyQuests:${playerId}`,
    () => generateDailyQuestsDb(playerId),
    () => generateDailyQuestsMemory(playerId)
  );
}

/**
 * Generate daily quests using database storage
 */
async function generateDailyQuestsDb(playerId: string): Promise<ActiveQuest[]> {
  // Check if already has quests for today
  if (await hasQuestsTodayDb(playerId)) {
    console.log(`Player ${playerId} already has quests for today`);
    return getActiveQuestsFromDb(playerId);
  }

  // Get all quest templates
  const allQuests = await getQuestTemplatesFromDb();

  if (allQuests.length === 0) {
    throw new Error('No quest templates found in database');
  }

  // Organize by difficulty
  const easyQuests = allQuests.filter(q => q.difficulty === 'easy');
  const mediumQuests = allQuests.filter(q => q.difficulty === 'medium');
  const hardQuests = allQuests.filter(q => q.difficulty === 'hard');

  if (!easyQuests.length || !mediumQuests.length || !hardQuests.length) {
    throw new Error(
      `Missing quest templates - easy: ${easyQuests.length}, medium: ${mediumQuests.length}, hard: ${hardQuests.length}`
    );
  }

  // Randomly select: 1 easy, 1 medium, 1 hard
  const selected: QuestTemplateRow[] = [
    easyQuests[Math.floor(Math.random() * easyQuests.length)],
    mediumQuests[Math.floor(Math.random() * mediumQuests.length)],
    hardQuests[Math.floor(Math.random() * hardQuests.length)],
  ];

  // Assign to player
  await assignQuestsToDb(playerId, selected);

  console.log(`Assigned ${selected.length} daily quests to player ${playerId}`);

  return getActiveQuestsFromDb(playerId);
}

/**
 * Generate daily quests using in-memory storage
 */
function generateDailyQuestsMemory(playerId: string): ActiveQuest[] {
  // Check if already has quests for today
  if (hasQuestsTodayMemory(playerId)) {
    console.log(`Player ${playerId} already has quests for today`);
    return getActiveQuestsMemory(playerId);
  }

  // Organize templates by difficulty
  const easyQuests = QUEST_TEMPLATES.filter(q => q.difficulty === 'easy');
  const mediumQuests = QUEST_TEMPLATES.filter(q => q.difficulty === 'medium');
  const hardQuests = QUEST_TEMPLATES.filter(q => q.difficulty === 'hard');

  if (!easyQuests.length || !mediumQuests.length || !hardQuests.length) {
    console.error('Missing quest templates');
    return [];
  }

  // Randomly select: 1 easy, 1 medium, 1 hard
  const selected: QuestTemplate[] = [
    easyQuests[Math.floor(Math.random() * easyQuests.length)],
    mediumQuests[Math.floor(Math.random() * mediumQuests.length)],
    hardQuests[Math.floor(Math.random() * hardQuests.length)],
  ];

  // Assign to player
  let quests = playerQuests.get(playerId);
  if (!quests) {
    quests = [];
    playerQuests.set(playerId, quests);
  }

  const today = new Date();

  for (const template of selected) {
    const questId = generateQuestId();

    quests.push({
      id: questId,
      templateType: template.type,
      assignedDate: today,
      progress: 0,
      completed: false,
      claimed: false,
    });

  }

  console.log(`Assigned ${selected.length} daily quests to player ${playerId}`);

  return getActiveQuestsMemory(playerId);
}

/**
 * Update progress on quest of given type.
 * Called after relevant player actions.
 */
export async function updateQuestProgress(
  playerId: string,
  questType: QuestType,
  amount = 1,
  player?: DiamondAwardPlayer
): Promise<ActiveQuest | null> {
  const result = await runWithStorageFallback(
    `updateQuestProgress:${playerId}:${questType}`,
    () => updateQuestProgressByTypeDb(playerId, questType, amount, player),
    () => updateQuestProgressMemory(playerId, questType, amount)
  );

  // Deepen the loop alongside the base daily quest:
  // 1. Always advance the weekly challenge if it matches this action type.
  updateWeeklyChallengeProgress(playerId, questType, amount, player);

  // 2. Advance any already-unlocked chain follow-up for this type.
  updateChainProgress(playerId, questType, amount, player);

  // 3. If the base daily quest just completed, unlock its chain follow-up and
  //    record a full-clear (fires the streak bonus when all dailies are done).
  if (result?.justCompleted) {
    unlockQuestChain(playerId, questType);
    await checkAndRecordFullClear(playerId, player);
  }

  return result;
}

/**
 * Update quest progress using database storage
 */
async function updateQuestProgressByTypeDb(
  playerId: string,
  questType: QuestType,
  amount: number,
  _player?: DiamondAwardPlayer
): Promise<ActiveQuest | null> {
  // Find active quest of this type
  const quest = await findActiveQuestByTypeDb(playerId, questType);

  if (!quest) {
    // No active quest of this type today
    return null;
  }

  // Update progress (cap at required amount)
  const newProgress = Math.min(quest.progress + amount, quest.progress_required);

  await updateQuestProgressDb(quest.id, newProgress);

  console.log(`Player ${playerId} quest '${questType}' progress: ${newProgress}/${quest.progress_required}`);

  // Check if completed
  if (newProgress >= quest.progress_required) {
    await markQuestCompletedDb(quest.id, playerId);

    console.log(`Player ${playerId} completed quest ${quest.id}`);

    return {
      id: String(quest.id),
      questType: quest.quest_type,
      description: quest.description,
      progress: newProgress,
      progressRequired: quest.progress_required,
      diamondReward: quest.diamond_reward,
      difficulty: quest.difficulty,
      iconName: quest.icon_name,
      completed: true,
      justCompleted: true,
    };
  }

  return {
    id: String(quest.id),
    questType: quest.quest_type,
    description: quest.description,
    progress: newProgress,
    progressRequired: quest.progress_required,
    diamondReward: quest.diamond_reward,
    difficulty: quest.difficulty,
    iconName: quest.icon_name,
    completed: false,
  };
}

/**
 * Update quest progress using in-memory storage
 */
function updateQuestProgressMemory(
  playerId: string,
  questType: QuestType,
  amount: number
): ActiveQuest | null {
  const quests = playerQuests.get(playerId);
  if (!quests) return null;

  const todayKey = getTodayKey();

  // Find active quest of this type for today
  const questRecord = quests.find(
    q =>
      q.templateType === questType &&
      q.assignedDate.toISOString().split('T')[0] === todayKey &&
      !q.completed
  );

  if (!questRecord) return null;

  const template = getQuestTemplate(questType);
  if (!template) return null;

  // Update progress (cap at required amount)
  questRecord.progress = Math.min(questRecord.progress + amount, template.required);

  console.log(`Player ${playerId} quest '${questType}' progress: ${questRecord.progress}/${template.required}`);

  // Check if completed
  if (questRecord.progress >= template.required) {
    questRecord.completed = true;

    return {
      id: questRecord.id,
      questType: template.type,
      description: template.desc,
      progress: questRecord.progress,
      progressRequired: template.required,
      diamondReward: template.reward,
      difficulty: template.difficulty,
      iconName: template.icon,
      completed: true,
      justCompleted: true,
    };
  }

  return {
    id: questRecord.id,
    questType: template.type,
    description: template.desc,
    progress: questRecord.progress,
    progressRequired: template.required,
    diamondReward: template.reward,
    difficulty: template.difficulty,
    iconName: template.icon,
    completed: false,
  };
}

/**
 * Get player's current daily quests
 */
export async function getActiveQuests(playerId: string): Promise<ActiveQuest[]> {
  return runWithStorageFallback(
    `getActiveQuests:${playerId}`,
    () => getActiveQuestsFromDb(playerId),
    () => getActiveQuestsMemory(playerId)
  );
}

/**
 * Get active quests from in-memory storage
 */
function getActiveQuestsMemory(playerId: string): ActiveQuest[] {
  const quests = playerQuests.get(playerId);
  if (!quests) return [];

  const todayKey = getTodayKey();
  const result: ActiveQuest[] = [];

  for (const q of quests) {
    if (q.assignedDate.toISOString().split('T')[0] !== todayKey) {
      continue;
    }

    const template = getQuestTemplate(q.templateType);
    if (!template) continue;

    const quest: ActiveQuest = {
      id: q.id,
      questType: template.type,
      description: template.desc,
      progress: q.progress,
      progressRequired: template.required,
      diamondReward: template.reward,
      difficulty: template.difficulty,
      iconName: template.icon,
      completed: q.completed,
    };

    if (q.claimed && q.completedDate) {
      quest.completedDate = q.completedDate.toISOString();
    }

    result.push(quest);
  }

  // Sort by difficulty: hard, medium, easy
  const order: Record<QuestDifficulty, number> = { hard: 0, medium: 1, easy: 2 };
  result.sort((a, b) => order[a.difficulty] - order[b.difficulty]);

  return result;
}

/**
 * Format quest data to match iOS DailyQuest model
 */
export function formatQuestForClient(quest: ActiveQuest): FormattedQuest {
  // Map quest_type to appropriate category
  const typeToCategory: Record<QuestType, string> = {
    talk_to_characters: 'social',
    socialize: 'social',
    go_on_date: 'social',
    increase_affinity: 'social',
    work_hours: 'career',
    earn_money: 'wealth',
    buy_item: 'wealth',
    attend_class: 'education',
    study: 'education',
    complete_activities: 'activities',
    spend_energy: 'activities',
  };

  const category = typeToCategory[quest.questType] ?? 'activities';

  // Check if quest is claimed (completedDate is set)
  const claimed = quest.completed && quest.completedDate !== undefined;

  return {
    id: quest.id,
    name: quest.description.substring(0, 30),
    description: quest.description,
    category: category.charAt(0).toUpperCase() + category.slice(1),
    reward: {
      diamonds: quest.diamondReward,
      energy: null,
      money: null,
    },
    progress: quest.progress,
    target: quest.progressRequired,
    completed: quest.completed,
    claimed,
  };
}

/**
 * Claim reward for a completed quest
 */
export async function claimQuestReward(
  playerId: string,
  questId: string,
  player?: DiamondAwardPlayer
): Promise<ClaimQuestResult> {
  return runWithStorageFallback(
    `claimQuestReward:${playerId}:${questId}`,
    () => claimQuestRewardDb(playerId, questId, player),
    () => claimQuestRewardMemory(playerId, questId, player)
  );
}

/**
 * Claim quest reward using database storage
 */
async function claimQuestRewardDb(
  playerId: string,
  questId: string,
  player?: DiamondAwardPlayer
): Promise<ClaimQuestResult> {
  const questIdNum = parseInt(questId, 10);

  if (isNaN(questIdNum)) {
    return { success: false, message: 'Invalid quest ID', reward: null };
  }

  // Get quest details
  const quest = await getQuestForClaimDb(questIdNum, playerId);

  if (!quest) {
    return { success: false, message: 'Quest not found', reward: null };
  }

  if (!quest.completed) {
    return { success: false, message: 'Quest not completed yet', reward: null };
  }

  if (quest.completed_at) {
    // Already claimed (completed_at is set when reward is claimed)
    return { success: false, message: 'Reward already claimed', reward: null };
  }

  // Mark as claimed by setting completed_at
  await setQuestClaimedDb(questIdNum, playerId);

  // Award diamonds
  awardDiamonds(playerId, 'Daily Quest Reward', quest.diamond_reward, player);

  console.log(`Player ${playerId} claimed quest ${questId} reward: ${quest.diamond_reward} diamonds`);

  return {
    success: true,
    message: `Claimed ${quest.diamond_reward} diamonds!`,
    reward: {
      diamonds: quest.diamond_reward,
      energy: null,
      money: null,
    },
  };
}

/**
 * Claim quest reward using in-memory storage
 */
function claimQuestRewardMemory(
  playerId: string,
  questId: string,
  player?: DiamondAwardPlayer
): ClaimQuestResult {
  const quests = playerQuests.get(playerId);
  if (!quests) {
    return { success: false, message: 'No quests found', reward: null };
  }

  const questRecord = quests.find(q => q.id === questId);
  if (!questRecord) {
    return { success: false, message: 'Quest not found', reward: null };
  }

  if (!questRecord.completed) {
    return { success: false, message: 'Quest not completed yet', reward: null };
  }

  if (questRecord.claimed) {
    return { success: false, message: 'Reward already claimed', reward: null };
  }

  const template = getQuestTemplate(questRecord.templateType);
  if (!template) {
    return { success: false, message: 'Quest template not found', reward: null };
  }

  // Mark as claimed
  questRecord.claimed = true;
  questRecord.completedDate = new Date();

  // Award diamonds
  awardDiamonds(playerId, 'Daily Quest Reward', template.reward, player);

  console.log(`Player ${playerId} claimed quest ${questId} reward: ${template.reward} diamonds`);

  return {
    success: true,
    message: `Claimed ${template.reward} diamonds!`,
    reward: {
      diamonds: template.reward,
      energy: null,
      money: null,
    },
  };
}

/**
 * Get player's quest completion statistics
 */
export async function getQuestStatistics(playerId: string): Promise<QuestStatistics> {
  return runWithStorageFallback(
    `getQuestStatistics:${playerId}`,
    () => getQuestStatisticsDb(playerId),
    () => getQuestStatisticsMemory(playerId)
  );
}

/**
 * Get quest statistics from database
 */
async function getQuestStatisticsDb(playerId: string): Promise<QuestStatistics> {
  // Get total completed quests
  const stats = await queryOne<StatsRow>(
    `SELECT
       COUNT(*) as total_completed,
       SUM(qt.diamond_reward) as total_diamonds_earned,
       COUNT(DISTINCT pq.quest_date) as days_completed
     FROM player_daily_quests pq
     JOIN daily_quest_templates qt ON pq.quest_id = qt.id
     WHERE pq.player_id = ? AND pq.completed = TRUE`,
    [playerId]
  );

  // Get completion rate by difficulty
  const byDifficultyRows = await query<DifficultyCountRow[]>(
    `SELECT qt.difficulty,
            COUNT(*) as completed_count
     FROM player_daily_quests pq
     JOIN daily_quest_templates qt ON pq.quest_id = qt.id
     WHERE pq.player_id = ? AND pq.completed = TRUE
     GROUP BY qt.difficulty`,
    [playerId]
  );

  const byDifficulty: Record<string, number> = {};
  for (const row of byDifficultyRows) {
    byDifficulty[row.difficulty] = row.completed_count;
  }

  return {
    totalCompleted: stats?.total_completed ?? 0,
    totalDiamondsEarned: stats?.total_diamonds_earned ?? 0,
    daysWithQuests: stats?.days_completed ?? 0,
    byDifficulty,
  };
}

/**
 * Get quest statistics from in-memory storage
 */
function getQuestStatisticsMemory(playerId: string): QuestStatistics {
  const quests = playerQuests.get(playerId);

  if (!quests) {
    return {
      totalCompleted: 0,
      totalDiamondsEarned: 0,
      daysWithQuests: 0,
      byDifficulty: {},
    };
  }

  const completed = quests.filter(q => q.completed);
  const uniqueDates = new Set(completed.map(q => q.assignedDate.toISOString().split('T')[0]));

  let totalDiamonds = 0;
  const byDifficulty: Record<string, number> = {};

  for (const quest of completed) {
    const template = getQuestTemplate(quest.templateType);
    if (template) {
      totalDiamonds += template.reward;
      byDifficulty[template.difficulty] = (byDifficulty[template.difficulty] ?? 0) + 1;
    }
  }

  return {
    totalCompleted: completed.length,
    totalDiamondsEarned: totalDiamonds,
    daysWithQuests: uniqueDates.size,
    byDifficulty,
  };
}

/**
 * Handle daily quest check (WebSocket handler)
 * Called when player connects to server or at midnight.
 */
export async function handleDailyQuestCheck(
  playerId: string,
  sendToClient: (playerId: string, message: Record<string, unknown>) => void
): Promise<void> {
  try {
    // Check if player has quests for today
    let activeQuests = await getActiveQuests(playerId);

    if (activeQuests.length === 0) {
      // Generate new quests
      activeQuests = await generateDailyQuests(playerId);
    }

    // Format quests for client
    const formattedQuests = activeQuests.map(formatQuestForClient);

    // Calculate reset times
    const today = getToday();
    const tomorrow = new Date(today);
    tomorrow.setDate(tomorrow.getDate() + 1);

    // Send complete state matching iOS DailyQuestsState model
    sendToClient(playerId, {
      type: 'dailyQuestsStatus',
      quests: formattedQuests,
      lastResetDate: today.toISOString(),
      nextResetDate: tomorrow.toISOString(),
    });

    console.log(`Sent daily quests state to player ${playerId}: ${formattedQuests.length} quests`);
  } catch (error) {
    console.error(`Error handling daily quest check for player ${playerId}:`, error);
  }
}

// ============================================
// Quest Chains (public API)
// ============================================

/**
 * Notify the chain system that a base daily quest completed. If that quest type
 * has a chain follow-up, unlock it for today (idempotent within the day).
 * Returns the newly-unlocked chain step, or null if none.
 */
export function unlockQuestChain(playerId: string, baseType: QuestType): QuestChainStep | null {
  const step = QUEST_CHAINS[baseType];
  if (!step) return null;

  const state = getEngagement(playerId);
  const today = getTodayKey();
  if (state.chainDate !== today) {
    // New day — reset the day's chain progress.
    state.chainDate = today;
    state.chains = {};
  }

  if (!state.chains[baseType]) {
    state.chains[baseType] = { progress: 0, completed: false, claimed: false };
  }

  return step;
}

/** Is a chain follow-up currently unlocked for this base quest type today? */
export function isChainUnlocked(playerId: string, baseType: QuestType): boolean {
  const state = getEngagement(playerId);
  return state.chainDate === getTodayKey() && !!state.chains[baseType];
}

/**
 * Advance progress on an unlocked chain follow-up. Only counts if the chain was
 * unlocked (i.e. the base quest already completed). Returns the chain state with
 * a `justCompleted` flag, awarding diamonds on completion. Null if not unlocked.
 */
export function updateChainProgress(
  playerId: string,
  baseType: QuestType,
  amount = 1,
  player?: DiamondAwardPlayer
): { step: QuestChainStep; progress: number; completed: boolean; justCompleted: boolean } | null {
  const step = QUEST_CHAINS[baseType];
  if (!step) return null;

  const state = getEngagement(playerId);
  if (state.chainDate !== getTodayKey() || !state.chains[baseType]) {
    return null; // not unlocked yet
  }

  const chain = state.chains[baseType];
  if (chain.completed) {
    return { step, progress: chain.progress, completed: true, justCompleted: false };
  }

  chain.progress = Math.min(chain.progress + amount, step.required);
  let justCompleted = false;

  if (chain.progress >= step.required) {
    chain.completed = true;
    justCompleted = true;
    if (!chain.claimed) {
      chain.claimed = true;
      awardDiamonds(playerId, `Quest Chain: ${baseType}`, step.reward, player);
    }
  }

  return { step, progress: chain.progress, completed: chain.completed, justCompleted };
}

// ============================================
// All-Quests Streak Bonus (public API)
// ============================================

function dayDiff(a: string, b: string): number {
  const da = new Date(`${a}T00:00:00Z`).getTime();
  const db = new Date(`${b}T00:00:00Z`).getTime();
  return Math.round((da - db) / 86400000);
}

/**
 * Record that the player cleared ALL of today's daily quests. Advances the
 * consecutive full-clear streak (resetting it if a day was skipped) and fires a
 * one-time-per-streak-milestone bonus once the streak reaches
 * STREAK_BONUS_THRESHOLD. Idempotent for a given day.
 */
export function recordFullClear(
  playerId: string,
  player?: DiamondAwardPlayer,
  todayKey: string = getTodayKey()
): StreakBonusResult {
  const state = getEngagement(playerId);

  if (state.lastFullClearDate === todayKey) {
    // Already recorded today — no change.
    return { fired: false, streakDays: state.fullClearStreak, diamonds: 0 };
  }

  if (state.lastFullClearDate && dayDiff(todayKey, state.lastFullClearDate) === 1) {
    state.fullClearStreak += 1;
  } else {
    state.fullClearStreak = 1;
  }
  state.lastFullClearDate = todayKey;

  // Fire bonus at every Nth consecutive day (3, 6, 9, ...), once per day.
  if (
    state.fullClearStreak >= STREAK_BONUS_THRESHOLD &&
    state.fullClearStreak % STREAK_BONUS_THRESHOLD === 0 &&
    state.lastStreakBonusDate !== todayKey
  ) {
    state.lastStreakBonusDate = todayKey;
    awardDiamonds(playerId, 'All-Quests Streak Bonus', STREAK_BONUS_REWARD, player);
    return { fired: true, streakDays: state.fullClearStreak, diamonds: STREAK_BONUS_REWARD };
  }

  return { fired: false, streakDays: state.fullClearStreak, diamonds: 0 };
}

/**
 * Convenience: check whether the player has completed every active daily quest
 * and, if so, record the full clear (firing the streak bonus when due). Returns
 * the streak result (fired=false if not all complete or already recorded).
 */
export async function checkAndRecordFullClear(
  playerId: string,
  player?: DiamondAwardPlayer
): Promise<StreakBonusResult> {
  const quests = await getActiveQuests(playerId);
  if (quests.length === 0 || !quests.every(q => q.completed)) {
    return { fired: false, streakDays: getEngagement(playerId).fullClearStreak, diamonds: 0 };
  }
  return recordFullClear(playerId, player);
}

export function getFullClearStreak(playerId: string): number {
  return getEngagement(playerId).fullClearStreak;
}

// ============================================
// Weekly Challenge (public API)
// ============================================

/**
 * Get (rolling if needed) the player's weekly challenge for the current week. A
 * new challenge is deterministically rolled when the week changes; progress
 * accumulates across the week.
 */
export function getOrRollWeeklyChallenge(playerId: string): ActiveWeeklyChallenge {
  const state = getEngagement(playerId);
  const weekKey = getWeekKey();

  if (!state.weekly || state.weekly.weekKey !== weekKey) {
    // Deterministic roll keyed by player + week so it's stable across calls.
    const idx = Math.abs(hashString(`${playerId}:${weekKey}`)) % WEEKLY_CHALLENGE_TEMPLATES.length;
    const t = WEEKLY_CHALLENGE_TEMPLATES[idx];
    state.weekly = {
      id: t.id,
      questType: t.type,
      description: t.desc,
      progress: 0,
      progressRequired: t.required,
      diamondReward: t.reward,
      completed: false,
      claimed: false,
      weekKey,
      iconName: t.icon,
    };
  }

  return { ...state.weekly };
}

function hashString(s: string): number {
  let h = 0;
  for (let i = 0; i < s.length; i++) {
    h = (h << 5) - h + s.charCodeAt(i);
    h |= 0;
  }
  return h;
}

/**
 * Advance the player's weekly challenge progress when an action matches the
 * challenge's quest type. Awards the (larger) diamond payout on completion.
 * Returns the updated challenge (with justCompleted) or null if the action does
 * not match the active challenge type.
 */
export function updateWeeklyChallengeProgress(
  playerId: string,
  questType: QuestType,
  amount = 1,
  player?: DiamondAwardPlayer
): (ActiveWeeklyChallenge & { justCompleted: boolean }) | null {
  const challenge = getOrRollWeeklyChallenge(playerId);
  if (challenge.questType !== questType || challenge.completed) {
    return null;
  }

  const state = getEngagement(playerId);
  const weekly = state.weekly!;
  weekly.progress = Math.min(weekly.progress + amount, weekly.progressRequired);
  let justCompleted = false;

  if (weekly.progress >= weekly.progressRequired) {
    weekly.completed = true;
    justCompleted = true;
    if (!weekly.claimed) {
      weekly.claimed = true;
      awardDiamonds(playerId, `Weekly Challenge: ${weekly.id}`, weekly.diamondReward, player);
    }
  }

  return { ...weekly, justCompleted };
}

// ============================================
// Persistence (mirrors lifeGoals model)
// ============================================

export interface PersistedQuestEngagement {
  fullClearStreak: number;
  lastFullClearDate: string | null;
  lastStreakBonusDate: string | null;
  weekly: ActiveWeeklyChallenge | null;
}

/**
 * Loose persisted shape accepted from the player blob. `weekly.questType` is a
 * plain string on disk; we validate/narrow it back to QuestType on load.
 */
export interface PersistedQuestEngagementInput {
  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;
}

/**
 * Hydrate a player's quest-engagement state (streak + weekly challenge) from a
 * persisted snapshot. Chains are intentionally day-scoped and NOT persisted.
 */
export function loadQuestEngagement(
  playerId: string,
  persisted?: PersistedQuestEngagementInput | null
): void {
  const state = getEngagement(playerId);
  state.fullClearStreak = typeof persisted?.fullClearStreak === 'number' ? persisted.fullClearStreak : 0;
  state.lastFullClearDate = persisted?.lastFullClearDate ?? null;
  state.lastStreakBonusDate = persisted?.lastStreakBonusDate ?? null;
  state.weekly = persisted?.weekly
    ? { ...persisted.weekly, questType: persisted.weekly.questType as QuestType }
    : null;
  state.chainDate = null;
  state.chains = {};
}

/** Serialize the player's quest-engagement state for the player JSON blob. */
export function serializeQuestEngagement(playerId: string): PersistedQuestEngagement {
  const state = getEngagement(playerId);
  return {
    fullClearStreak: state.fullClearStreak,
    lastFullClearDate: state.lastFullClearDate,
    lastStreakBonusDate: state.lastStreakBonusDate,
    weekly: state.weekly ? { ...state.weekly } : null,
  };
}

// ============================================
// Testing/Utility Functions
// ============================================

/**
 * Clear player quests (for testing or new game)
 */
export function clearPlayerQuests(playerId: string): void {
  playerQuests.delete(playerId);
  questEngagement.delete(playerId);
}

/**
 * Clear all quests (for testing)
 */
export function clearAllQuests(): void {
  playerQuests.clear();
  questEngagement.clear();
  questIdCounter = 0;
}

/**
 * Get all quest templates
 */
export function getAllQuestTemplates(): QuestTemplate[] {
  return [...QUEST_TEMPLATES];
}

/**
 * Check if database storage is enabled
 */
export function isDatabaseStorageEnabled(): boolean {
  return useDatabaseStorage;
}
