/**
 * Character Memory System for BaoLife
 * Provides long-term memory for NPCs by extracting and storing important facts
 * from conversations. Enables characters to remember player preferences, past events,
 * and relationship history across sessions.
 * Ported from Python character_memory.py
 *
 * Features:
 * - AI-powered fact extraction from conversations
 * - Importance scoring for memory prioritization
 * - Memory decay over time (older memories fade)
 * - Context-aware memory retrieval
 */

import { getConnection } from '../../database/pool.js';
import { apiUsageTracker } from '../../monitoring/api_usage_tracker.js';
import { Person } from '../../models/Person.js';
import { ConversationObj, ConversationMessage } from './types.js';
import { aiProvider } from './ai_provider.js';

export interface MemoryFact {
  id?: number;
  fact: string;
  importance: number;
  learned_date: Date;
  last_accessed?: Date;
  access_count?: number;
}

export interface MemoryDecayConfig {
  /** Days until a memory starts decaying (default: 7) */
  decayStartDays: number;
  /** Minimum importance after full decay (default: 1) */
  minImportance: number;
  /** Decay rate per day after start (default: 0.5) */
  decayRatePerDay: number;
}

export const DEFAULT_DECAY_CONFIG: MemoryDecayConfig = {
  decayStartDays: 7,
  minImportance: 1,
  decayRatePerDay: 0.5,
};


// ============================================================================
// Character Memory Class
// ============================================================================

/**
 * Persistent memory system for NPC characters.
 * Extracts and stores important facts from conversations.
 */
export class CharacterMemory {
  private characterId: string;
  private playerId: string;
  private facts: MemoryFact[] = [];
  private loaded = false;
  private decayConfig: MemoryDecayConfig;

  constructor(
    characterId: string,
    playerId: string,
    decayConfig: MemoryDecayConfig = DEFAULT_DECAY_CONFIG
  ) {
    this.characterId = characterId;
    this.playerId = playerId;
    this.decayConfig = decayConfig;
  }

  /**
   * Extract important facts from recent conversation messages.
   * Called periodically (e.g., every 5 messages) to update memory.
   * Uses AI to extract facts and score their importance.
   */
  async extractFacts(
    conversation: ConversationObj,
    character: Person
  ): Promise<string[]> {
    // Only extract if we have enough new messages
    if (conversation.conversation.length < 5) {
      return [];
    }

    // Get last 5 messages for fact extraction
    const recentMessages = conversation.conversation.slice(-5);

    // Format messages for extraction
    const conversationText = this.formatMessagesForExtraction(
      recentMessages,
      character
    );

    // Create extraction prompt with importance scoring
    const extractionPrompt = `Extract key facts from this conversation that ${character.firstname} should remember about the player.

Conversation:
${conversationText}

Extract facts with importance scores (1-10) in this format (one per line):
- [importance:X] [Fact about player's preferences, plans, or important information]

Importance scoring guide:
- 10: Life-changing events (birth, death, marriage, major illness)
- 8-9: Major relationship milestones, career changes, important personal revelations
- 6-7: Preferences, ongoing plans, recurring topics of interest
- 4-5: Casual preferences, one-time mentions of activities
- 1-3: Minor details, passing comments

Only extract genuinely important facts (max 3). If nothing important, respond with "None".
Facts should be specific and actionable for future conversations.

Facts:`;

    try {
      // Call API for fact extraction with timeout
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 5000);

      const result = await aiProvider.client.chat.completions.create(
        {
          model: aiProvider.model,
          messages: [{ role: 'user', content: extractionPrompt }],
          max_tokens: 300,
          temperature: 0.2, // Low temperature for factual extraction
        },
        { signal: controller.signal }
      );

      clearTimeout(timeoutId);

      const factsText = result.choices[0]?.message?.content?.trim() || '';

      // Track API usage for fact extraction
      if (result.usage) {
        try {
          const cost = await apiUsageTracker.trackUsage(
            this.playerId,
            conversation.id || null,
            aiProvider.model,
            {
              prompt_tokens: result.usage.prompt_tokens,
              completion_tokens: result.usage.completion_tokens,
              total_tokens: result.usage.total_tokens,
            },
            'fact_extraction'
          );
          console.log(`Fact extraction cost: $${cost.toFixed(6)}`);
        } catch (e) {
          console.error('Failed to track fact extraction usage:', e);
        }
      }

      if (factsText.toLowerCase() !== 'none' && factsText) {
        // Parse facts with importance scores from response
        const parsedFacts = this.parseFactsWithImportance(factsText);

        if (parsedFacts.length > 0) {
          // Save facts to database with importance scores
          await this.saveFactsWithImportance(parsedFacts, conversation);

          // Add to local cache as full MemoryFact objects
          const now = new Date();
          const memoryFacts: MemoryFact[] = parsedFacts.map((f) => ({
            fact: f.fact,
            importance: f.importance,
            learned_date: now,
            access_count: 0,
          }));
          this.facts.push(...memoryFacts);

          console.log(
            `Extracted ${parsedFacts.length} new facts for ${character.firstname}`
          );
          return parsedFacts.map((f) => f.fact);
        }
      }
    } catch (error) {
      console.error('Failed to extract facts:', error);
    }

    return [];
  }

  /**
   * Parse facts with importance scores from AI response.
   * Expected format: "- [importance:X] Fact text"
   */
  private parseFactsWithImportance(
    factsText: string
  ): Array<{ fact: string; importance: number }> {
    const lines = factsText.split('\n').filter((f) => f.trim().startsWith('-'));
    const results: Array<{ fact: string; importance: number }> = [];

    for (const line of lines) {
      // Remove leading "- " and parse importance
      const content = line.trim().substring(2).trim();
      const importanceMatch = content.match(/^\[importance:(\d+)\]\s*/i);

      if (importanceMatch) {
        const importance = Math.min(10, Math.max(1, parseInt(importanceMatch[1], 10)));
        const fact = content.replace(importanceMatch[0], '').trim();
        if (fact) {
          results.push({ fact, importance });
        }
      } else {
        // Fallback: calculate importance heuristically if not provided
        const importance = this.calculateImportanceHeuristic(content);
        if (content) {
          results.push({ fact: content, importance });
        }
      }
    }

    return results;
  }

  /**
   * Calculate importance score heuristically when AI doesn't provide one.
   * Uses keywords and content analysis.
   */
  private calculateImportanceHeuristic(fact: string): number {
    const lowerFact = fact.toLowerCase();

    // High importance keywords (8-10)
    const highImportanceKeywords = [
      'died', 'death', 'born', 'birth', 'married', 'engaged', 'divorced',
      'pregnant', 'cancer', 'illness', 'accident', 'fired', 'promoted',
      'graduated', 'love', 'hate', 'afraid', 'trauma'
    ];
    if (highImportanceKeywords.some((k) => lowerFact.includes(k))) {
      return 9;
    }

    // Medium-high importance keywords (6-7)
    const mediumHighKeywords = [
      'favorite', 'dream', 'goal', 'plan', 'always', 'never', 'relationship',
      'family', 'job', 'career', 'moving', 'holiday', 'anniversary'
    ];
    if (mediumHighKeywords.some((k) => lowerFact.includes(k))) {
      return 7;
    }

    // Medium importance keywords (4-5)
    const mediumKeywords = [
      'like', 'prefer', 'enjoy', 'hobby', 'interested', 'weekend',
      'friend', 'work', 'school'
    ];
    if (mediumKeywords.some((k) => lowerFact.includes(k))) {
      return 5;
    }

    // Default: base importance on length (longer = slightly more important)
    return Math.min(6, Math.max(3, Math.floor(fact.length / 15)));
  }

  /**
   * Extract facts from a specific set of messages.
   * Unlike extractFacts(), this accepts an explicit message list,
   * allowing the caller to pass the full batch since last extraction.
   */
  async extractFactsFromMessages(
    messages: ConversationMessage[],
    character: Person
  ): Promise<string[]> {
    if (messages.length < 2) return [];

    const conversationText = this.formatMessagesForExtraction(messages, character);

    const extractionPrompt = `Extract key facts from this conversation that ${character.firstname} should remember about the player.

Conversation:
${conversationText}

Extract facts with importance scores (1-10) in this format (one per line):
- [importance:X] [Fact about player's preferences, plans, or important information]

Importance scoring guide:
- 10: Life-changing events (birth, death, marriage, major illness)
- 8-9: Major relationship milestones, career changes, important personal revelations
- 6-7: Preferences, ongoing plans, recurring topics of interest
- 4-5: Casual preferences, one-time mentions of activities
- 1-3: Minor details, passing comments

Only extract genuinely important facts (max 5). If nothing important, respond with "None".
Facts should be specific and actionable for future conversations.

Facts:`;

    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 8000); // Slightly longer for bigger batches

      const result = await aiProvider.client.chat.completions.create(
        {
          model: aiProvider.model,
          messages: [{ role: 'user', content: extractionPrompt }],
          max_tokens: 400, // Slightly more room for bigger batches
          temperature: 0.2,
        },
        { signal: controller.signal }
      );

      clearTimeout(timeoutId);

      const factsText = result.choices[0]?.message?.content?.trim() || '';

      if (result.usage) {
        try {
          const cost = await apiUsageTracker.trackUsage(
            this.playerId,
            null,
            aiProvider.model,
            {
              prompt_tokens: result.usage.prompt_tokens,
              completion_tokens: result.usage.completion_tokens,
              total_tokens: result.usage.total_tokens,
            },
            'fact_extraction'
          );
          console.log(`Fact extraction cost: $${cost.toFixed(6)}`);
        } catch (e) {
          console.error('Failed to track fact extraction usage:', e);
        }
      }

      if (factsText.toLowerCase() !== 'none' && factsText) {
        const parsedFacts = this.parseFactsWithImportance(factsText);
        if (parsedFacts.length > 0) {
          // Create a minimal ConversationObj for saveFactsWithImportance
          const fakeConvo = { id: null } as any;
          await this.saveFactsWithImportance(parsedFacts, fakeConvo);

          const now = new Date();
          const memoryFacts: MemoryFact[] = parsedFacts.map((f) => ({
            fact: f.fact,
            importance: f.importance,
            learned_date: now,
            access_count: 0,
          }));
          this.facts.push(...memoryFacts);

          return parsedFacts.map((f) => f.fact);
        }
      }
    } catch (error) {
      console.error('Failed to extract facts from messages:', error);
    }

    return [];
  }

  /**
   * Format messages for fact extraction
   */
  private formatMessagesForExtraction(
    messages: ConversationMessage[],
    character: Person
  ): string {
    return messages
      .map((msg) => {
        const speaker =
          msg.sender !== this.characterId ? 'Player' : character.firstname;
        return `${speaker}: ${msg.message}`;
      })
      .join('\n');
  }

  /**
   * Load character's memory facts from database.
   * Applies memory decay to older facts.
   */
  async loadFacts(limit = 10): Promise<string[]> {
    if (this.loaded && this.facts.length > 0) {
      return this.facts.map((f) => f.fact);
    }

    let connection;
    try {
      connection = await getConnection();
      const [rows] = await connection.execute(
        `SELECT id, fact, importance, learned_date, last_accessed, access_count
         FROM character_memory
         WHERE character_id = ? AND player_id = ?
         ORDER BY importance DESC, learned_date DESC
         LIMIT ?`,
        [this.characterId, this.playerId, String(limit)]
      );

      const results = rows as MemoryFact[];

      // Apply memory decay and sort by effective importance
      this.facts = results.map((row) => ({
        ...row,
        importance: this.applyMemoryDecay(row.importance, row.learned_date, row.last_accessed),
      }));

      // Sort by decayed importance
      this.facts.sort((a, b) => b.importance - a.importance);

      this.loaded = true;

      console.log(
        `Loaded ${this.facts.length} facts for character ${this.characterId}`
      );
      return this.facts.map((f) => f.fact);
    } catch (error) {
      console.error('Failed to load character memory:', error);
      // Table might not exist yet, return empty list
      return [];
    } finally {
      if (connection) {
        connection.release();
      }
    }
  }

  /**
   * Apply memory decay based on how old a memory is.
   * Uses the more recent of learned_date or last_accessed as the decay anchor,
   * so memories that keep coming up in conversation resist decay.
   */
  private applyMemoryDecay(importance: number, learnedDate: Date, lastAccessed?: Date): number {
    const now = new Date();
    // Use the more recent date as decay anchor — accessed memories reset their decay clock
    const anchorDate = lastAccessed && new Date(lastAccessed).getTime() > new Date(learnedDate).getTime()
      ? new Date(lastAccessed)
      : new Date(learnedDate);
    const daysSinceAnchor = Math.floor(
      (now.getTime() - anchorDate.getTime()) / (1000 * 60 * 60 * 24)
    );

    // No decay within the grace period
    if (daysSinceAnchor <= this.decayConfig.decayStartDays) {
      return importance;
    }

    // Calculate decay
    const daysOfDecay = daysSinceAnchor - this.decayConfig.decayStartDays;
    const decayAmount = daysOfDecay * this.decayConfig.decayRatePerDay;
    const decayedImportance = importance - decayAmount;

    // Ensure minimum importance
    return Math.max(this.decayConfig.minImportance, decayedImportance);
  }

  /**
   * Reinforce a memory by accessing it.
   * Increases access count and updates last_accessed timestamp.
   * This helps important memories resist decay.
   */
  async reinforceMemory(factId: number): Promise<void> {
    let connection;
    try {
      connection = await getConnection();
      await connection.execute(
        `UPDATE character_memory
         SET last_accessed = NOW(), access_count = COALESCE(access_count, 0) + 1
         WHERE id = ?`,
        [factId]
      );
    } catch (error) {
      console.error('Failed to reinforce memory:', error);
    } finally {
      if (connection) {
        connection.release();
      }
    }
  }

  /**
   * Get relevant facts for context injection.
   * Returns facts sorted by effective importance (with decay applied).
   */
  async getRelevantFacts(maxFacts = 5): Promise<string[]> {
    if (!this.loaded) {
      await this.loadFacts();
    }

    // Return most important facts (decay already applied during load)
    return this.facts.slice(0, maxFacts).map((f) => f.fact);
  }

  /**
   * Get relevant facts with metadata for advanced use cases.
   */
  async getRelevantFactsWithMetadata(maxFacts = 5): Promise<MemoryFact[]> {
    if (!this.loaded) {
      await this.loadFacts();
    }

    return this.facts.slice(0, maxFacts);
  }

  /**
   * Get formatted memory context for prompt injection.
   * Reinforces accessed memories so they resist decay.
   */
  async getMemoryContext(): Promise<string> {
    const facts = await this.getRelevantFactsWithMetadata();

    if (facts.length === 0) {
      return '';
    }

    // Reinforce memories that are being used in context (fire-and-forget)
    for (const fact of facts) {
      if (fact.id) {
        this.reinforceMemory(fact.id).catch(() => {});
      }
    }

    // Format facts for prompt
    const factsText = facts.map(f => f.fact).join('; ');
    return `You remember these things about the player: ${factsText}`;
  }

  /**
   * Get context-aware memories based on current conversation topic.
   * Uses AI to find the most relevant memories for the current context.
   */
  async getContextualMemories(
    currentTopic: string,
    maxFacts = 3
  ): Promise<string[]> {
    if (!this.loaded) {
      await this.loadFacts();
    }

    if (this.facts.length === 0) {
      return [];
    }

    // For small memory sets, return all
    if (this.facts.length <= maxFacts) {
      return this.facts.map((f) => f.fact);
    }

    // Use AI to select the most relevant memories for the current topic
    const allFacts = this.facts.map((f) => f.fact).join('\n- ');

    const prompt = `Given the current conversation topic, select the most relevant memories.

Current topic: ${currentTopic}

Available memories:
- ${allFacts}

Select the ${maxFacts} most relevant memories for this topic. Return only the memory text, one per line.
If none are relevant, return "None".`;

    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 3000);

      const result = await aiProvider.client.chat.completions.create(
        {
          model: aiProvider.model,
          messages: [{ role: 'user', content: prompt }],
          max_tokens: 200,
          temperature: 0.1,
        },
        { signal: controller.signal }
      );

      clearTimeout(timeoutId);

      // Track contextual memory retrieval API cost
      if (result.usage) {
        await apiUsageTracker.trackUsage(
          this.playerId,
          null,
          aiProvider.model,
          {
            prompt_tokens: result.usage.prompt_tokens,
            completion_tokens: result.usage.completion_tokens,
            total_tokens: result.usage.total_tokens,
          },
          'memory_retrieval'
        ).catch(e => console.error('Failed to track memory retrieval cost:', e));
      }

      const responseText = result.choices[0]?.message?.content?.trim() || '';

      if (responseText.toLowerCase() === 'none') {
        return [];
      }

      // Parse selected memories
      const selectedMemories = responseText
        .split('\n')
        .map((line) => line.replace(/^-\s*/, '').trim())
        .filter((line) => line.length > 0);

      const selectedFacts = selectedMemories.slice(0, maxFacts);

      // Reinforce memories that were selected as relevant (fire-and-forget)
      for (const selected of selectedFacts) {
        const match = this.facts.find(f => f.fact === selected || selected.includes(f.fact));
        if (match?.id) {
          this.reinforceMemory(match.id).catch(() => {});
        }
      }

      return selectedFacts;
    } catch (error) {
      console.error('Failed to get contextual memories:', error);
      // Fallback: return top memories by importance
      const fallback = this.facts.slice(0, maxFacts);
      for (const f of fallback) {
        if (f.id) this.reinforceMemory(f.id).catch(() => {});
      }
      return fallback.map((f) => f.fact);
    }
  }

  /**
   * Save extracted facts to database with AI-provided importance scores.
   */
  private async saveFactsWithImportance(
    facts: Array<{ fact: string; importance: number }>,
    conversation: ConversationObj
  ): Promise<void> {
    let connection;
    try {
      connection = await getConnection();

      // Batch insert facts, skipping exact duplicates
      let inserted = 0;
      for (const { fact, importance } of facts) {
        // Check for existing exact match to prevent duplicate facts
        const [existing] = await connection.execute(
          `SELECT id FROM character_memory
           WHERE character_id = ? AND player_id = ? AND fact = ?
           LIMIT 1`,
          [this.characterId, this.playerId, fact]
        );
        if ((existing as any[]).length > 0) {
          console.log(`Skipping duplicate fact: "${fact.substring(0, 50)}..."`);
          continue;
        }

        await connection.execute(
          `INSERT INTO character_memory
           (character_id, player_id, fact, importance, conversation_id, learned_date, access_count)
           VALUES (?, ?, ?, ?, ?, NOW(), 0)`,
          [
            this.characterId,
            this.playerId,
            fact,
            importance,
            conversation.id || null,
          ]
        );
        inserted++;
      }

      console.log(`Saved ${inserted}/${facts.length} facts to database (${facts.length - inserted} duplicates skipped)`);
    } catch (error) {
      console.error('Failed to save facts to database:', error);
      console.error('Facts were:', facts);
      // Don't crash if table doesn't exist yet
    } finally {
      if (connection) {
        connection.release();
      }
    }
  }

  /**
   * Clear all facts for this character-player relationship
   */
  async clearFacts(): Promise<void> {
    let connection;
    try {
      connection = await getConnection();
      await connection.execute(
        `DELETE FROM character_memory
         WHERE character_id = ? AND player_id = ?`,
        [this.characterId, this.playerId]
      );

      this.facts = [];
      this.loaded = false;
      console.log(`Cleared all facts for character ${this.characterId}`);
    } catch (error) {
      console.error('Failed to clear facts:', error);
    } finally {
      if (connection) {
        connection.release();
      }
    }
  }

  /**
   * Clean up decayed memories that are no longer useful.
   * Removes memories that have decayed below a threshold.
   */
  async pruneDecayedMemories(threshold = 2): Promise<number> {
    let connection;
    try {
      connection = await getConnection();

      // Get memories that have decayed significantly
      const [rows] = await connection.execute(
        `SELECT id, importance, learned_date, last_accessed
         FROM character_memory
         WHERE character_id = ? AND player_id = ?`,
        [this.characterId, this.playerId]
      );

      const results = rows as MemoryFact[];
      const toDelete: number[] = [];

      for (const row of results) {
        const decayedImportance = this.applyMemoryDecay(
          row.importance,
          row.learned_date,
          row.last_accessed
        );
        if (decayedImportance < threshold && row.id) {
          toDelete.push(row.id);
        }
      }

      if (toDelete.length > 0) {
        const placeholders = toDelete.map(() => '?').join(',');
        await connection.execute(
          `DELETE FROM character_memory WHERE id IN (${placeholders})`,
          toDelete
        );
        console.log(`Pruned ${toDelete.length} decayed memories`);

        // Refresh local cache
        this.loaded = false;
        this.facts = [];
      }

      return toDelete.length;
    } catch (error) {
      console.error('Failed to prune decayed memories:', error);
      return 0;
    } finally {
      if (connection) {
        connection.release();
      }
    }
  }

  /**
   * Get memory statistics for debugging/analytics.
   */
  async getMemoryStats(): Promise<{
    totalMemories: number;
    averageImportance: number;
    oldestMemory: Date | null;
    newestMemory: Date | null;
  }> {
    let connection;
    try {
      connection = await getConnection();
      const [rows] = await connection.execute(
        `SELECT
           COUNT(*) as total,
           AVG(importance) as avgImportance,
           MIN(learned_date) as oldest,
           MAX(learned_date) as newest
         FROM character_memory
         WHERE character_id = ? AND player_id = ?`,
        [this.characterId, this.playerId]
      );

      const result = (rows as any[])[0];
      return {
        totalMemories: result.total || 0,
        averageImportance: result.avgImportance || 0,
        oldestMemory: result.oldest || null,
        newestMemory: result.newest || null,
      };
    } catch (error) {
      console.error('Failed to get memory stats:', error);
      return {
        totalMemories: 0,
        averageImportance: 0,
        oldestMemory: null,
        newestMemory: null,
      };
    } finally {
      if (connection) {
        connection.release();
      }
    }
  }
}

// ============================================================================
// Table Creation
// ============================================================================

/**
 * Create character_memory table if it doesn't exist.
 * Call this during server startup or migration.
 */
export async function createCharacterMemoryTable(): Promise<void> {
  let connection;
  try {
    connection = await getConnection();
    await connection.execute(`
      CREATE TABLE IF NOT EXISTS character_memory (
        id INT AUTO_INCREMENT PRIMARY KEY,
        character_id VARCHAR(255) NOT NULL,
        player_id VARCHAR(255) NOT NULL,
        fact TEXT NOT NULL,
        learned_date DATETIME NOT NULL,
        importance TINYINT DEFAULT 5,
        conversation_id VARCHAR(255),
        last_accessed DATETIME,
        access_count INT DEFAULT 0,
        INDEX idx_char_player (character_id, player_id),
        INDEX idx_importance (importance DESC),
        INDEX idx_learned_date (learned_date DESC)
      )
    `);
    console.log('Character memory table created/verified');

    await connection.execute(`
      ALTER TABLE character_memory
        MODIFY COLUMN character_id VARCHAR(255) NOT NULL,
        MODIFY COLUMN player_id VARCHAR(255) NOT NULL,
        MODIFY COLUMN conversation_id VARCHAR(255) NULL
    `);

    // Add new columns if they don't exist (for migration)
    try {
      await connection.execute(`
        ALTER TABLE character_memory
        ADD COLUMN IF NOT EXISTS last_accessed DATETIME,
        ADD COLUMN IF NOT EXISTS access_count INT DEFAULT 0
      `);
    } catch {
      // Columns might already exist or syntax not supported - that's fine
    }
  } catch (error) {
    console.error('Failed to create character_memory table:', error);
  } finally {
    if (connection) {
      connection.release();
    }
  }
}

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

/**
 * Store a single memory for a character.
 * Convenience function for direct memory storage without extraction.
 */
export async function storeMemory(
  characterId: string,
  playerId: string,
  fact: string,
  importance = 5,
  conversationId?: string
): Promise<void> {
  let connection;
  try {
    connection = await getConnection();
    await connection.execute(
      `INSERT INTO character_memory
       (character_id, player_id, fact, importance, conversation_id, learned_date, access_count)
       VALUES (?, ?, ?, ?, ?, NOW(), 0)`,
      [characterId, playerId, fact, importance, conversationId || null]
    );
    console.log(`Stored memory for character ${characterId}`);
  } catch (error) {
    console.error('Failed to store memory:', error);
  } finally {
    if (connection) {
      connection.release();
    }
  }
}

/**
 * Retrieve memories for a character.
 * Convenience function for quick memory retrieval.
 */
export async function retrieveMemories(
  characterId: string,
  playerId: string,
  limit = 10
): Promise<string[]> {
  const memory = new CharacterMemory(characterId, playerId);
  return await memory.loadFacts(limit);
}

/**
 * Apply memory decay globally for all characters.
 * Run this periodically (e.g., daily) to maintain memory hygiene.
 */
export async function applyGlobalMemoryDecay(
  decayConfig: MemoryDecayConfig = DEFAULT_DECAY_CONFIG
): Promise<{ processed: number; pruned: number }> {
  let connection;
  let processed = 0;
  let pruned = 0;

  try {
    connection = await getConnection();

    // Get all unique character-player pairs
    const [pairs] = await connection.execute(`
      SELECT DISTINCT character_id, player_id
      FROM character_memory
    `);

    for (const pair of pairs as Array<{ character_id: string; player_id: string }>) {
      const memory = new CharacterMemory(pair.character_id, pair.player_id, decayConfig);
      const prunedCount = await memory.pruneDecayedMemories();
      pruned += prunedCount;
      processed++;
    }

    console.log(`Processed ${processed} character-player pairs, pruned ${pruned} memories`);
  } catch (error) {
    console.error('Failed to apply global memory decay:', error);
  } finally {
    if (connection) {
      connection.release();
    }
  }

  return { processed, pruned };
}

// ============================================================================
// Recurring-NPC Callbacks (T011b)
// ============================================================================
//
// Make NPCs feel persistent by letting them reference a player's PRIOR choices
// — specifically the durable `flags` set by major catalog/conversation choices
// (now persisted on the Player, so they survive restart). This is intentionally
// a pure, DB-free string builder: the conversation layer can splice the returned
// lines into an NPC's context so the character "remembers" what the player did,
// without a DB round-trip on every turn.

/**
 * Human-readable callback phrasings for known persistent choice flags. Keep this
 * map small and curated — it covers the high-emotion life decisions where an NPC
 * referencing them lands. Unknown flags are simply skipped.
 */
const FLAG_CALLBACKS: Record<string, string> = {
  'lateLife.retired': 'the player recently retired and is starting a new chapter',
  'lateLife.estatePlanned': 'the player has put their affairs and estate in order',
  'lateLife.reconciledChild': 'the player reached out to reconcile with an estranged child',
  'career.startedBusiness': 'the player took the leap and started their own business',
  'career.businessSideHustle': 'the player is building a business on the side',
  'career.wasLaidOff': 'the player was laid off and is finding their footing again',
};

/**
 * Build short, in-context callback lines from a player's persistent choice flags
 * so an NPC can reference prior decisions and feel like they remember the
 * player's life. Accepts either the in-memory Set or the post-JSON array form of
 * `flags` (mirroring how the rest of the codebase reads that container).
 *
 * Pure and DB-free — safe to call on every conversation turn. Returns at most
 * `maxCallbacks` lines, most-relevant-first by insertion order of the flags.
 */
export function buildPlayerChoiceCallback(
  flags: Set<string> | string[] | undefined,
  maxCallbacks = 2
): string[] {
  const list = flags instanceof Set ? Array.from(flags) : Array.isArray(flags) ? flags : [];
  const callbacks: string[] = [];
  for (const flag of list) {
    const phrasing = FLAG_CALLBACKS[flag];
    if (phrasing) {
      callbacks.push(`You remember that ${phrasing}.`);
    }
    if (callbacks.length >= maxCallbacks) {
      break;
    }
  }
  return callbacks;
}

/**
 * Persist a single durable "the player did X" memory for an NPC, so the choice
 * resurfaces in future conversations even after the flag-based callbacks rotate
 * out. Thin convenience wrapper over storeMemory with a sensible high importance
 * for life-decision callbacks.
 */
export async function recordPlayerChoiceMemory(
  characterId: string,
  playerId: string,
  choiceDescription: string,
  importance = 8
): Promise<void> {
  await storeMemory(characterId, playerId, choiceDescription, importance);
}

// ============================================================================
// Export
// ============================================================================

export const characterMemoryManager = {
  CharacterMemory,
  createCharacterMemoryTable,
  storeMemory,
  retrieveMemories,
  applyGlobalMemoryDecay,
  buildPlayerChoiceCallback,
  recordPlayerChoiceMemory,
  DEFAULT_DECAY_CONFIG,
};
