// NPCManager with event → knot auto-mapping and conversation history
// OPTIMIZED: InkEngine caching, event listener cleanup, debug logging
// default export NPCManager
import { isInLineOfSight, drawLOSCone, clearLOSCone, getNPCFacingDirection } from './npc-los.js';

export default class NPCManager {
  constructor(eventDispatcher, barkSystem = null) {
    this.eventDispatcher = eventDispatcher;
    this.barkSystem = barkSystem;
    this.npcs = new Map();
    this.eventListeners = new Map(); // Track registered listeners for cleanup
    this.triggeredEvents = new Map(); // Track which events have been triggered per NPC
    this.conversationHistory = new Map(); // Track conversation history per NPC: { npcId: [ {type, text, timestamp, choiceText} ] }
    this.timedMessages = []; // Scheduled messages: { npcId, text, triggerTime, delivered, phoneId }
    this.timedConversations = []; // Scheduled conversations: { npcId, targetKnot, triggerTime, delivered }
    this.gameStartTime = Date.now(); // Track when game started for timed messages
    this.timerInterval = null; // Timer for checking timed messages
    
    // OPTIMIZATION: Cache InkEngine instances and fetched stories
    this.inkEngineCache = new Map(); // { npcId: inkEngine }
    this.storyCache = new Map(); // { storyPath: storyJson }
    
    // LOS Visualization
    this.losVisualizations = new Map(); // { npcId: graphicsObject }
    this.losVisualizationEnabled = false; // Toggle LOS cone rendering
    
    // OPTIMIZATION: Debug mode (set via window.NPC_DEBUG = true)
    this.debug = false;
  }

  /**
   * OPTIMIZATION: Log helper with debug mode
   */
  _log(level, message, data = null) {
    if (!this.debug && level !== 'error' && level !== 'warn') return;
    
    const prefix = {
      error: '❌',
      warn: '⚠️',
      info: 'ℹ️',
      debug: '🔍'
    }[level] || '📍';
    
    if (data) {
      console[level](`${prefix} ${message}`, data);
    } else {
      console[level](`${prefix} ${message}`);
    }
  }

  // registerNPC(id, opts) or registerNPC({ id, ...opts })
  // opts: { 
  //   displayName, storyPath, avatar, currentKnot, 
  //   phoneId: 'player_phone' | 'office_phone' | null,  // Which phone this NPC uses
  //   npcType: 'phone' | 'sprite',  // Text-only phone NPC or in-world sprite
  //   eventMappings: { 'event_pattern': { knot, bark, once, cooldown } } 
  // }
  registerNPC(id, opts = {}) {
    // Accept either registerNPC(id, opts) or registerNPC({ id, ...opts })
    let realId = id;
    let realOpts = opts;
    if (typeof id === 'object' && id !== null) {
      realOpts = id;
      realId = id.id;
    }
    if (!realId) throw new Error('registerNPC requires an id');
    
    const entry = Object.assign({ 
      id: realId, 
      displayName: realId, 
      metadata: {},
      eventMappings: {},
      phoneId: 'player_phone',  // Default to player's phone
      npcType: 'phone',  // Default to phone-based NPC
      itemsHeld: []  // Initialize empty inventory for NPC item giving
    }, realOpts);
    
    this.npcs.set(realId, entry);
    
    // Initialize conversation history for this NPC
    if (!this.conversationHistory.has(realId)) {
      this.conversationHistory.set(realId, []);
    }
    
    // Set up event listeners for auto-mapping
    if (entry.eventMappings && this.eventDispatcher) {
      this._setupEventMappings(realId, entry.eventMappings);
    }
    
    // Schedule timed messages if any are defined
    if (entry.timedMessages && Array.isArray(entry.timedMessages)) {
      entry.timedMessages.forEach(msg => {
        this.scheduleTimedMessage({
          npcId: realId,
          text: msg.message,
          delay: msg.delay,
          phoneId: entry.phoneId
        });
      });
      console.log(`[NPCManager] Scheduled ${entry.timedMessages.length} timed messages for ${realId}`);
    }
    
    // Schedule timed conversations if any are defined
    if (entry.timedConversation) {
      this.scheduleTimedConversation({
        npcId: realId,
        targetKnot: entry.timedConversation.targetKnot,
        delay: entry.timedConversation.delay,
        background: entry.timedConversation.background // Optional background image
      });
      console.log(`[NPCManager] Scheduled timed conversation for ${realId} to knot: ${entry.timedConversation.targetKnot}`);
    }
    
    return entry;
  }

  getNPC(id) {
    return this.npcs.get(id) || null;
  }

  /**
   * Check if any NPC in a room should trigger person-chat instead of lockpicking
   * Considers NPC line-of-sight and facing direction
   * Returns the NPC if one should handle lockpick_used_in_view with person-chat
   * Otherwise returns null
   */
  shouldInterruptLockpickingWithPersonChat(roomId, playerPosition = null) {
    if (!roomId) return null;
    
    console.log(`👁️ [LOS CHECK] shouldInterruptLockpickingWithPersonChat: roomId="${roomId}", playerPos=${playerPosition ? `(${playerPosition.x.toFixed(0)}, ${playerPosition.y.toFixed(0)})` : 'null'}`);
    
    for (const npc of this.npcs.values()) {
      // NPC must be in the specified room and be a 'person' type NPC
      if (npc.roomId !== roomId || npc.npcType !== 'person') continue;
      
      console.log(`👁️ [LOS CHECK] Checking NPC: "${npc.id}" (room: ${npc.roomId}, type: ${npc.npcType})`);
      
      // Check if NPC has lockpick_used_in_view event mapping with person-chat
      if (npc.eventMappings && Array.isArray(npc.eventMappings)) {
        const lockpickMapping = npc.eventMappings.find(mapping => 
          mapping.eventPattern === 'lockpick_used_in_view' && 
          mapping.conversationMode === 'person-chat'
        );
        
        if (!lockpickMapping) {
          console.log(`👁️ [LOS CHECK]   ✗ NPC has no lockpick_used_in_view mapping`);
          continue;
        }
        
        console.log(`👁️ [LOS CHECK]   ✓ NPC has lockpick_used_in_view→person-chat mapping`);
        
        // Check LOS configuration
        const losConfig = npc.los || {
          enabled: true,
          range: 300,    // Default detection range
          angle: 120     // Default 120° field of view
        };
        
        // If player position provided, check if player is in LOS
        if (playerPosition) {
          // Get detailed information for debugging
          // Try to get sprite from npc._sprite (how it's stored), then npc.sprite, then npc position
          const sprite = npc._sprite || npc.sprite;
          const npcPos = (sprite && typeof sprite.getCenter === 'function') ? 
            sprite.getCenter() : 
            { x: npc.x ?? 0, y: npc.y ?? 0 };
          
          console.log(`👁️ [LOS CHECK]   npcPos: ${npcPos ? `(${npcPos.x}, ${npcPos.y})` : 'NULL'}, losConfig: range=${losConfig.range}, angle=${losConfig.angle}`);
          
          // Ensure npcPos is valid before using
          if (npcPos && npcPos.x !== undefined && npcPos.y !== undefined && 
              !Number.isNaN(npcPos.x) && !Number.isNaN(npcPos.y)) {
            const distance = Math.sqrt(
              Math.pow(playerPosition.x - npcPos.x, 2) + 
              Math.pow(playerPosition.y - npcPos.y, 2)
            );
            
            // Calculate angle to player
            const angleRad = Math.atan2(playerPosition.y - npcPos.y, playerPosition.x - npcPos.x);
            const angleToPlayer = (angleRad * 180 / Math.PI + 360) % 360;
            
            // Get NPC facing direction for debugging
            const npcFacing = getNPCFacingDirection(npc);
            
            const inLOS = isInLineOfSight(npc, playerPosition, losConfig);
            console.log(`👁️ [LOS CHECK]   NPC Facing: ${npcFacing.toFixed(1)}°, Distance: ${distance.toFixed(1)}px (range: ${losConfig.range}), Angle: ${angleToPlayer.toFixed(1)}° (FOV: ${losConfig.angle}°), LOS: ${inLOS}`);
            
            if (!inLOS) {
              console.log(
                `👁️ NPC "${npc.id}" CANNOT see player\n` +
                `   Position: NPC(${npcPos.x.toFixed(0)}, ${npcPos.y.toFixed(0)}) → Player(${playerPosition.x.toFixed(0)}, ${playerPosition.y.toFixed(0)})\n` +
                `   Distance: ${distance.toFixed(1)}px (range: ${losConfig.range}px) ${distance > losConfig.range ? '❌ TOO FAR' : '✅ in range'}\n` +
                `   Angle to Player: ${angleToPlayer.toFixed(1)}° (FOV: ${losConfig.angle}°)`
              );
              continue;
            }
          } else {
            console.log(`👁️ [LOS CHECK]   Position invalid, checking LOS anyway...`);
            if (!isInLineOfSight(npc, playerPosition, losConfig)) {
              // Position unavailable but still check LOS detection
              continue;
            }
          }
        }
        
        console.log(`�🚫 INTERRUPTING LOCKPICKING: NPC "${npc.id}" in room "${roomId}" can see player and has person-chat mapped to lockpick event`);
        return npc;
      }
    }
    
    return null;
  }

  // Set bark system (can be set after construction)
  setBarkSystem(barkSystem) {
    this.barkSystem = barkSystem;
  }

  // Add a message to conversation history (internal method)
  addMessageToHistory(npcId, type, text) {
    if (!this.conversationHistory.has(npcId)) {
      this.conversationHistory.set(npcId, []);
    }
    this.conversationHistory.get(npcId).push({
      type,
      text,
      timestamp: Date.now(),
      choiceText: null
    });
    this._log('debug', `Added ${type} message to ${npcId} history:`, text);
  }

  // Public API: Add a message with full metadata (used by external systems)
  addMessage(npcId, type, text, metadata = {}) {
    if (!this.conversationHistory.has(npcId)) {
      this.conversationHistory.set(npcId, []);
    }
    this.conversationHistory.get(npcId).push({
      type,
      text,
      timestamp: Date.now(),
      read: type === 'player', // Player messages are automatically marked as read
      ...metadata
    });
    this._log('debug', `Added ${type} message to ${npcId}:`, text);
  }

  // Get conversation history for an NPC
  getConversationHistory(npcId) {
    return this.conversationHistory.get(npcId) || [];
  }

  // Clear conversation history for an NPC
  clearConversationHistory(npcId) {
    this.conversationHistory.set(npcId, []);
  }

  // Get all NPCs for a specific phone
  getNPCsByPhone(phoneId) {
    return Array.from(this.npcs.values()).filter(npc => npc.phoneId === phoneId);
  }

  // Get total unread message count for a phone
  getTotalUnreadCount(phoneId, allowedNpcIds = null) {
    let npcs = this.getNPCsByPhone(phoneId);
    
    // Filter to only allowed NPCs if specified
    if (allowedNpcIds && allowedNpcIds.length > 0) {
      npcs = npcs.filter(npc => allowedNpcIds.includes(npc.id));
    }
    
    let totalUnread = 0;
    
    for (const npc of npcs) {
      const history = this.getConversationHistory(npc.id);
      const unreadCount = history.filter(msg => !msg.read && msg.type === 'npc').length;
      totalUnread += unreadCount;
    }
    
    return totalUnread;
  }

  // Set up event listeners for an NPC's event mappings
  _setupEventMappings(npcId, eventMappings) {
    if (!this.eventDispatcher) return;
    
    console.log(`📋 Setting up event mappings for ${npcId}:`, eventMappings);
    
    // Handle both array format (from JSON) and object format
    const mappingsArray = Array.isArray(eventMappings) 
      ? eventMappings 
      : Object.entries(eventMappings).map(([pattern, config]) => ({
          eventPattern: pattern,
          ...(typeof config === 'string' ? { targetKnot: config } : config)
        }));
    
    for (const mapping of mappingsArray) {
      const eventPattern = mapping.eventPattern;
      const config = {
        knot: mapping.targetKnot || mapping.knot,
        bark: mapping.bark,
        once: mapping.onceOnly || mapping.once,
        cooldown: mapping.cooldown,
        condition: mapping.condition,
        maxTriggers: mapping.maxTriggers,  // Add max trigger limit
        conversationMode: mapping.conversationMode  // Add conversation mode (e.g., 'person-chat')
      };
      
      console.log(`  📌 Registering listener for event: ${eventPattern} → ${config.knot}`);
      
      const listener = (eventData) => {
        this._handleEventMapping(npcId, eventPattern, config, eventData);
      };
      
      // Register listener with event dispatcher
      this.eventDispatcher.on(eventPattern, listener);
      
      // Track listener for cleanup
      if (!this.eventListeners.has(npcId)) {
        this.eventListeners.set(npcId, []);
      }
      this.eventListeners.get(npcId).push({ pattern: eventPattern, listener });
    }
    
    console.log(`✅ Registered ${mappingsArray.length} event mappings for ${npcId}`);
  }

  // Handle when a mapped event fires
  _handleEventMapping(npcId, eventPattern, config, eventData) {
    console.log(`🎯 Event triggered: ${eventPattern} for NPC: ${npcId}`, eventData);
    
    const npc = this.getNPC(npcId);
    if (!npc) {
      console.warn(`⚠️ NPC ${npcId} not found`);
      return;
    }
    
    // Check if event should be handled
    const eventKey = `${npcId}:${eventPattern}`;
    const triggered = this.triggeredEvents.get(eventKey) || { count: 0, lastTime: 0 };
    
    // Check if this is a once-only event that's already triggered
    if (config.once && triggered.count > 0) {
      console.log(`⏭️ Skipping once-only event ${eventPattern} (already triggered)`);
      return;
    }
    
    // Check if max triggers reached
    if (config.maxTriggers && triggered.count >= config.maxTriggers) {
      console.log(`🚫 Event ${eventPattern} has reached max triggers (${config.maxTriggers})`);
      return;
    }
    
    // Check cooldown (in milliseconds, default 5000ms = 5s)
    // IMPORTANT: Use ?? instead of || to properly handle cooldown: 0
    const cooldown = config.cooldown !== undefined && config.cooldown !== null ? config.cooldown : 5000;
    const now = Date.now();
    if (triggered.lastTime && (now - triggered.lastTime < cooldown)) {
      const remainingMs = cooldown - (now - triggered.lastTime);
      console.log(`⏸️ Event ${eventPattern} on cooldown (${remainingMs}ms remaining)`);
      return;
    }
    
    // Check condition if provided (can be string or function)
    if (config.condition) {
      let conditionMet = false;
      
      if (typeof config.condition === 'function') {
        conditionMet = config.condition(eventData, npc);
      } else if (typeof config.condition === 'string') {
        // Evaluate condition string as JavaScript
        try {
          const data = eventData; // Make 'data' available in eval scope
          conditionMet = eval(config.condition);
        } catch (error) {
          console.error(`❌ Error evaluating condition: ${config.condition}`, error);
          return;
        }
      }
      
      if (!conditionMet) {
        console.log(`🚫 Event ${eventPattern} condition not met:`, config.condition);
        return;
      }
    }
    
    console.log(`✅ Event ${eventPattern} conditions passed, triggering NPC reaction`);
    
    // Update triggered tracking
    triggered.count++;
    triggered.lastTime = now;
    this.triggeredEvents.set(eventKey, triggered);
    
    // Update NPC's current knot if specified
    if (config.knot) {
      npc.currentKnot = config.knot;
      console.log(`📍 Updated ${npcId} current knot to: ${config.knot}`);
    }
    
    // Debug: Log the full config to see what we're working with
    console.log(`🔍 Event config for ${eventPattern}:`, {
      conversationMode: config.conversationMode,
      npcType: npc.npcType,
      knot: config.knot,
      fullConfig: config
    });
    
    // Check if this event should trigger a full person-chat conversation
    // instead of just a bark (indicated by conversationMode: 'person-chat')
    if (config.conversationMode === 'person-chat' && npc.npcType === 'person') {
      console.log(`👤 Handling person-chat for event on NPC ${npcId}`);
      
      // CHECK: Is a conversation already active with this NPC?
      const currentConvNPCId = window.currentConversationNPCId;
      const activeMinigame = window.MinigameFramework?.currentMinigame;
      const isPersonChatActive = activeMinigame?.constructor?.name === 'PersonChatMinigame';
      const isConversationActive = currentConvNPCId === npcId;
      
      console.log(`🔍 Event jump check:`, {
        targetNpcId: npcId,
        currentConvNPCId: currentConvNPCId,
        isConversationActive: isConversationActive,
        activeMinigame: activeMinigame?.constructor?.name || 'none',
        isPersonChatActive: isPersonChatActive,
        hasJumpToKnot: typeof activeMinigame?.jumpToKnot === 'function'
      });
      
      if (isConversationActive && isPersonChatActive) {
        // JUMP TO KNOT in the active conversation instead of starting a new one
        console.log(`⚡ Active conversation detected with ${npcId}, attempting jump to knot: ${config.knot}`);
        
        if (typeof activeMinigame.jumpToKnot === 'function') {
          try {
            const jumpSuccess = activeMinigame.jumpToKnot(config.knot);
            if (jumpSuccess) {
              console.log(`✅ Successfully jumped to knot ${config.knot} in active conversation`);
              return;  // Success - exit early
            } else {
              console.warn(`⚠️ Failed to jump to knot, falling back to new conversation`);
            }
          } catch (error) {
            console.error(`❌ Error during jumpToKnot: ${error.message}`);
          }
        } else {
          console.warn(`⚠️ jumpToKnot method not available on minigame`);
        }
      } else {
        console.log(`ℹ️ Not jumping: isConversationActive=${isConversationActive}, isPersonChatActive=${isPersonChatActive}`);
      }
      
      // Not in an active conversation OR jump failed - start a new person-chat minigame
      console.log(`👤 Starting new person-chat conversation for NPC ${npcId}`);
      
      // Close any currently running minigame (like lockpicking) first
      if (window.MinigameFramework && window.MinigameFramework.currentMinigame) {
        console.log(`🛑 Closing currently running minigame before starting person-chat`);
        window.MinigameFramework.endMinigame(false, null);
        console.log(`✅ Closed current minigame`);
      }
      
      // Start the person-chat minigame
      if (window.MinigameFramework) {
        console.log(`✅ Starting person-chat minigame for ${npcId}`);
        window.MinigameFramework.startMinigame('person-chat', null, {
          npcId: npc.id,
          startKnot: config.knot || npc.currentKnot,
          scenario: window.gameScenario
        });
        console.log(`[NPCManager] Event '${eventPattern}' triggered for NPC '${npcId}' → person-chat conversation`);
        return;  // Exit early - person-chat is handling it
      } else {
        console.warn(`⚠️ MinigameFramework not available for person-chat`);
      }
    }
    // If bark text is provided, show it directly
    if (this.barkSystem && (config.bark || config.message)) {
      const barkText = config.bark || config.message;
      
      // Add bark message to conversation history (marked as bark)
      this.addMessage(npcId, 'npc', barkText, { 
        eventPattern,
        knot: config.knot,
        isBark: true  // Flag this as a bark, not full conversation
      });
      
      console.log(`💬 Showing bark with direct message: ${barkText}`);
      
      this.barkSystem.showBark({
        npcId: npc.id,
        npcName: npc.displayName,
        message: barkText,
        avatar: npc.avatar,
        inkStoryPath: npc.storyPath,
        startKnot: config.knot || npc.currentKnot,
        phoneId: npc.phoneId
      });
    } 
    // Otherwise, if we have a knot, load the Ink story and get the text
    else if (this.barkSystem && config.knot && npc.storyPath) {
      console.log(`📖 Loading Ink story from knot: ${config.knot}`);
      
      // Load the Ink story and navigate to the knot
      this._showBarkFromKnot(npcId, npc, config.knot, eventPattern);
    }
    
    console.log(`[NPCManager] Event '${eventPattern}' triggered for NPC '${npcId}' → knot '${config.knot}'`);
  }
  
  // Load Ink story, navigate to knot, and show the text as a bark
  async _showBarkFromKnot(npcId, npc, knotName, eventPattern) {
    try {
      // OPTIMIZATION: Fetch story from cache or network
      let storyJson = this.storyCache.get(npc.storyPath);
      if (!storyJson) {
        const response = await fetch(npc.storyPath);
        if (!response.ok) {
          throw new Error(`Failed to load story: ${response.statusText}`);
        }
        storyJson = await response.json();
        // Cache for future use
        this.storyCache.set(npc.storyPath, storyJson);
      }
      
      // OPTIMIZATION: Reuse cached InkEngine or create new one
      let inkEngine = this.inkEngineCache.get(npcId);
      if (!inkEngine) {
        const { default: InkEngine } = await import('./ink/ink-engine.js?v=1');
        inkEngine = new InkEngine(npcId);
        inkEngine.loadStory(storyJson);
        this.inkEngineCache.set(npcId, inkEngine);
      }
      
      // Navigate to the knot
      inkEngine.goToKnot(knotName);
      
      // Get the text from the knot
      const result = inkEngine.continue();
      
      if (result.text) {
        
        // Add to conversation history (marked as bark)
        this.addMessage(npcId, 'npc', result.text, { 
          eventPattern,
          knot: knotName,
          isBark: true  // Flag this as a bark, not full conversation
        });
        
        // Show the bark
        this.barkSystem.showBark({
          npcId: npc.id,
          npcName: npc.displayName,
          message: result.text,
          avatar: npc.avatar,
          inkStoryPath: npc.storyPath,
          startKnot: knotName,
          phoneId: npc.phoneId
        });
      } else {
        console.warn(`⚠️ No text found in knot: ${knotName}`);
      }
    } catch (error) {
      console.error(`❌ Error loading bark from knot ${knotName}:`, error);
    }
  }

  // Helper to emit events about an NPC
  emit(npcId, type, payload = {}) {
    const ev = Object.assign({ npcId, type }, payload);
    this.eventDispatcher && this.eventDispatcher.emit(type, ev);
  }

  // Get all NPCs
  getAllNPCs() {
    return Array.from(this.npcs.values());
  }

  // Check if an event has been triggered for an NPC
  hasTriggered(npcId, eventPattern) {
    const eventKey = `${npcId}:${eventPattern}`;
    const triggered = this.triggeredEvents.get(eventKey);
    return triggered ? triggered.count > 0 : false;
  }

  // Schedule a timed message to be delivered after a delay
  // opts: { npcId, text, triggerTime (ms from game start) OR delay (ms from now), phoneId }
  scheduleTimedMessage(opts) {
    const { npcId, text, triggerTime, delay, phoneId } = opts;
    
    if (!npcId || !text) {
      console.error('[NPCManager] scheduleTimedMessage requires npcId and text');
      return;
    }
    
    // Use triggerTime if provided, otherwise use delay (defaults to 0)
    const actualTriggerTime = triggerTime !== undefined ? triggerTime : (delay || 0);
    
    this.timedMessages.push({
      npcId,
      text,
      triggerTime: actualTriggerTime, // milliseconds from game start
      phoneId: phoneId || 'player_phone',
      delivered: false
    });
    
    console.log(`[NPCManager] Scheduled timed message from ${npcId} at ${actualTriggerTime}ms:`, text);
  }

  // Schedule a timed conversation to start after a delay
  // Similar to timedMessages but for person NPCs (opens person-chat minigame)
  // 
  // opts: { npcId, targetKnot, triggerTime (ms from game start) OR delay (ms from now) }
  // 
  // Example: After 3 seconds, automatically open a conversation with test_npc_back at the "group_meeting" knot
  //   scheduleTimedConversation({
  //     npcId: 'test_npc_back',
  //     targetKnot: 'group_meeting',
  //     delay: 3000
  //   })
  //
  // USAGE IN SCENARIO JSON:
  //   {
  //     "id": "test_npc_back",
  //     "displayName": "Back NPC",
  //     "npcType": "person",
  //     "storyPath": "scenarios/ink/test2.json",
  //     "currentKnot": "hub",
  //     "timedConversation": {
  //       "delay": 3000,          // 3 seconds
  //       "targetKnot": "group_meeting"
  //     }
  //   }
  scheduleTimedConversation(opts) {
    const { npcId, targetKnot, triggerTime, delay, background } = opts;
    
    if (!npcId || !targetKnot) {
      console.error('[NPCManager] scheduleTimedConversation requires npcId and targetKnot');
      return;
    }
    
    // Use triggerTime if provided, otherwise use delay (defaults to 0)
    const actualTriggerTime = triggerTime !== undefined ? triggerTime : (delay || 0);
    
    this.timedConversations.push({
      npcId,
      targetKnot,
      triggerTime: actualTriggerTime, // milliseconds from game start
      background: background, // Optional background image path
      delivered: false
    });
    
    console.log(`[NPCManager] Scheduled timed conversation from ${npcId} at ${actualTriggerTime}ms to knot: ${targetKnot}`);
  }

  // Start checking for timed messages (call this when game starts)
  startTimedMessages() {
    if (this.timerInterval) {
      clearInterval(this.timerInterval);
    }
    
    this.gameStartTime = Date.now();
    
    // Check every second for messages that need to be delivered
    this.timerInterval = setInterval(() => {
      this._checkTimedMessages();
    }, 1000);
    
    console.log('[NPCManager] Started timed messages system');
  }

  // Stop checking for timed messages (cleanup)
  stopTimedMessages() {
    if (this.timerInterval) {
      clearInterval(this.timerInterval);
      this.timerInterval = null;
    }
  }

  // Check if any timed messages need to be delivered
  _checkTimedMessages() {
    const now = Date.now();
    const elapsed = now - this.gameStartTime;
    
    for (const message of this.timedMessages) {
      if (!message.delivered && elapsed >= message.triggerTime) {
        this._deliverTimedMessage(message);
        message.delivered = true;
      }
    }
    
    // Also check timed conversations
    for (const conversation of this.timedConversations) {
      if (!conversation.delivered && elapsed >= conversation.triggerTime) {
        this._deliverTimedConversation(conversation);
        conversation.delivered = true;
      }
    }
  }

  // Deliver a timed message (add to history and show bark)
  _deliverTimedMessage(message) {
    const npc = this.getNPC(message.npcId);
    if (!npc) {
      console.warn(`[NPCManager] Cannot deliver timed message: NPC ${message.npcId} not found`);
      return;
    }
    
    // Add message to conversation history
    this.addMessage(message.npcId, 'npc', message.text, { 
      timed: true,
      phoneId: message.phoneId
    });
    
    // Update phone badge if updatePhoneBadge function exists
    if (window.updatePhoneBadge && message.phoneId) {
      window.updatePhoneBadge(message.phoneId);
    }
    
    // Show bark notification
    if (this.barkSystem) {
      this.barkSystem.showBark({
        npcId: npc.id,
        npcName: npc.displayName,
        message: message.text,
        avatar: npc.avatar,
        inkStoryPath: npc.storyPath,
        startKnot: npc.currentKnot,
        phoneId: message.phoneId
      });
    }
    
    console.log(`[NPCManager] Delivered timed message from ${message.npcId}:`, message.text);
  }

  // Deliver a timed conversation (start person-chat minigame at specified knot)
  _deliverTimedConversation(conversation) {
    const npc = this.getNPC(conversation.npcId);
    if (!npc) {
      console.warn(`[NPCManager] Cannot deliver timed conversation: NPC ${conversation.npcId} not found`);
      return;
    }
    
    // Update NPC's current knot to the target knot
    npc.currentKnot = conversation.targetKnot;
    
    // Check if MinigameFramework is available to start the person-chat minigame
    if (window.MinigameFramework && typeof window.MinigameFramework.startMinigame === 'function') {
      console.log(`🎭 Starting timed conversation for ${conversation.npcId} at knot: ${conversation.targetKnot}`);
      
      window.MinigameFramework.startMinigame('person-chat', null, {
        npcId: conversation.npcId,
        title: npc.displayName || conversation.npcId,
        background: conversation.background // Optional background image path
      });
    } else {
      console.warn(`[NPCManager] MinigameFramework not available to start person-chat for timed conversation`);
    }
    
    console.log(`[NPCManager] Delivered timed conversation from ${conversation.npcId} to knot: ${conversation.targetKnot}`);
  }

  // Load timed messages from scenario data
  // timedMessages: [ { npcId, text, triggerTime, phoneId } ]
  loadTimedMessages(timedMessages) {
    if (!Array.isArray(timedMessages)) return;
    
    timedMessages.forEach(msg => {
      this.scheduleTimedMessage(msg);
    });
    
    console.log(`[NPCManager] Loaded ${timedMessages.length} timed messages`);
  }

  /**
   * Clear conversation history for an NPC (useful for testing/debugging)
   * @param {string} npcId - The NPC to reset
   */
  clearNPCHistory(npcId) {
    if (!npcId) {
      console.warn('[NPCManager] clearNPCHistory requires npcId');
      return;
    }
    
    // Clear conversation history
    if (this.conversationHistory.has(npcId)) {
      this.conversationHistory.set(npcId, []);
      console.log(`[NPCManager] Cleared conversation history for ${npcId}`);
    }
    
    // Clear story state from localStorage
    const storyStateKey = `npc_story_state_${npcId}`;
    if (localStorage.getItem(storyStateKey)) {
      localStorage.removeItem(storyStateKey);
      console.log(`[NPCManager] Cleared saved story state for ${npcId}`);
    }
    
    console.log(`✅ Reset NPC: ${npcId}. Start a new conversation to see fresh state.`);
  }

  /**
   * OPTIMIZATION: Clean up event listeners for an NPC
   * Call this when removing an NPC or changing scenes
   */
  unregisterNPC(npcId) {
    if (!this.eventDispatcher) return;

    // Remove all event listeners for this NPC
    const listeners = this.eventListeners.get(npcId);
    if (listeners) {
      for (const { pattern, listener } of listeners) {
        this.eventDispatcher.off(pattern, listener);
      }
      this.eventListeners.delete(npcId);
      console.log(`[NPCManager] Cleaned up ${listeners.length} event listeners for ${npcId}`);
    }

    // Clear cached InkEngine
    if (this.inkEngineCache.has(npcId)) {
      this.inkEngineCache.delete(npcId);
      console.log(`[NPCManager] Cleared cached InkEngine for ${npcId}`);
    }

    // Remove NPC from registry
    this.npcs.delete(npcId);
    this.conversationHistory.delete(npcId);
    this.triggeredEvents.delete(npcId);

    console.log(`[NPCManager] Unregistered NPC: ${npcId}`);
  }

  /**
   * OPTIMIZATION: Clean up all NPCs (call on scene change)
   */
  unregisterAllNPCs() {
    const npcIds = Array.from(this.npcs.keys());
    for (const npcId of npcIds) {
      this.unregisterNPC(npcId);
    }
    console.log(`[NPCManager] Cleaned up all NPCs (${npcIds.length} total)`);
  }

  /**
   * Get or create Ink engine for an NPC
   * Fetches story from NPC data and initializes InkEngine
   * @param {string} npcId - NPC ID
   * @returns {Promise<InkEngine|null>} Ink engine instance or null
   */
  async getInkEngine(npcId) {
    try {
      const npc = this.getNPC(npcId);
      if (!npc) {
        console.error(`❌ NPC not found: ${npcId}`);
        return null;
      }

      // Check if already cached
      if (this.inkEngineCache.has(npcId)) {
        console.log(`📖 Using cached InkEngine for ${npcId}`);
        return this.inkEngineCache.get(npcId);
      }

      // Need to load story
      if (!npc.storyPath) {
        console.error(`❌ NPC ${npcId} has no storyPath`);
        return null;
      }

      // Fetch story from cache or network
      let storyJson = this.storyCache.get(npc.storyPath);
      if (!storyJson) {
        console.log(`📚 Fetching story from ${npc.storyPath}`);
        const response = await fetch(npc.storyPath);
        if (!response.ok) {
          throw new Error(`Failed to load story: ${response.statusText}`);
        }
        storyJson = await response.json();
        this.storyCache.set(npc.storyPath, storyJson);
      }

      // Create and cache InkEngine
      const { default: InkEngine } = await import('./ink/ink-engine.js?v=1');
      const inkEngine = new InkEngine(npcId);
      inkEngine.loadStory(storyJson);

      // Import npcConversationStateManager for global variable sync
      const { default: npcConversationStateManager } = await import('./npc-conversation-state.js?v=2');

      // Discover any global_* variables not in scenario JSON
      npcConversationStateManager.discoverGlobalVariables(inkEngine.story);

      // Sync global variables from window.gameState to story
      npcConversationStateManager.syncGlobalVariablesToStory(inkEngine.story);

      // Observe changes to sync back to window.gameState
      npcConversationStateManager.observeGlobalVariableChanges(inkEngine.story, npcId);

      this.inkEngineCache.set(npcId, inkEngine);

      console.log(`✅ InkEngine initialized for ${npcId}`);
      return inkEngine;
    } catch (error) {
      console.error(`❌ Error getting InkEngine for ${npcId}:`, error);
      return null;
    }
  }

  /**
   * OPTIMIZATION: Destroy InkEngine cache for a specific story
   * Useful when memory is tight or story changed
   */
  clearStoryCache(storyPath) {
    this.storyCache.delete(storyPath);
  }

  /**
   * OPTIMIZATION: Clear all caches
   */
  clearAllCaches() {
    this.inkEngineCache.clear();
    this.storyCache.clear();
    console.log(`[NPCManager] Cleared all caches`);
  }

  /**
   * Enable or disable LOS cone visualization for debugging
   * @param {boolean} enable - Whether to show LOS cones
   * @param {Phaser.Scene} scene - Phaser scene for drawing
   */
  setLOSVisualization(enable, scene = null) {
    this.losVisualizationEnabled = enable;
    
    if (enable && scene) {
      console.log('👁️ Enabling LOS visualization');
      this._updateLOSVisualizations(scene);
    } else if (!enable) {
      console.log('👁️ Disabling LOS visualization');
      this._clearLOSVisualizations();
    }
  }

  /**
   * Update LOS visualizations for all NPCs in a scene
   * Call this from the game loop (update method) if visualization is enabled
   * @param {Phaser.Scene} scene - Phaser scene for drawing
   */
  updateLOSVisualizations(scene) {
    if (!this.losVisualizationEnabled || !scene) return;
    
    this._updateLOSVisualizations(scene);
  }

  /**
   * Internal: Update or create LOS cone graphics
   */
  _updateLOSVisualizations(scene) {
    // console.log(`🎯 Updating LOS visualizations for ${this.npcs.size} NPCs`);
    let visualizedCount = 0;
    
    for (const npc of this.npcs.values()) {
      // Only visualize person-type NPCs with LOS config
      if (npc.npcType !== 'person') {
        // console.log(`   Skip "${npc.id}" - not person type (${npc.npcType})`);
        continue;
      }
      
      if (!npc.los || !npc.los.enabled) {
        // console.log(`   Skip "${npc.id}" - no LOS config or disabled`);
        continue;
      }
      
      // console.log(`   Processing "${npc.id}" - has LOS config`, npc.los);
      
      // Remove old visualization
      if (this.losVisualizations.has(npc.id)) {
        // console.log(`   Clearing old visualization for "${npc.id}"`);
        clearLOSCone(this.losVisualizations.get(npc.id));
      }
      
      // Draw new cone (depth is set inside drawLOSCone)
      const graphics = drawLOSCone(scene, npc, npc.los, 0x00ff00, 0.15);
      if (graphics) {
        this.losVisualizations.set(npc.id, graphics);
        // Graphics depth is already set inside drawLOSCone to -999
        // console.log(`   ✅ Created visualization for "${npc.id}"`);
        visualizedCount++;
      } else {
        console.log(`   ❌ Failed to create visualization for "${npc.id}"`);
      }
    }
    
    // console.log(`✅ LOS visualization update complete: ${visualizedCount}/${this.npcs.size} visualized`);
  }

  /**
   * Internal: Clear all LOS visualizations
   */
  _clearLOSVisualizations() {
    for (const graphics of this.losVisualizations.values()) {
      clearLOSCone(graphics);
    }
    this.losVisualizations.clear();
  }

  /**
   * Cleanup: destroy all LOS visualizations and event listeners
   */
  destroy() {
    this._clearLOSVisualizations();
    this.stopTimedMessages();
    
    // Clear all event listeners
    for (const listeners of this.eventListeners.values()) {
      listeners.forEach(({ listener }) => {
        if (this.eventDispatcher && typeof listener === 'function') {
          this.eventDispatcher.off('*', listener);
        }
      });
    }
    this.eventListeners.clear();
    
    console.log('[NPCManager] Destroyed');
  }
}

// Console helper for debugging
if (typeof window !== 'undefined') {
  window.clearNPCHistory = (npcId) => {
    if (!window.npcManager) {
      console.error('NPCManager not available');
      return;
    }
    window.npcManager.clearNPCHistory(npcId);
  };
}
