/**
 * Talking Heads Plugin - Main Class
 * AI-generated 2D avatars with synchronized animations for TikTok users speaking via TTS
 */

const path = require('path');
const fs = require('fs').promises;

// Import engines and utilities
const AvatarGenerator = require('./engines/avatar-generator');
const SpriteGenerator = require('./engines/sprite-generator');
const AnimationController = require('./engines/animation-controller');
const CacheManager = require('./utils/cache-manager');
const RoleManager = require('./utils/role-manager');
const { getAllStyleTemplates, getStyleKeys } = require('./utils/style-templates');

class TalkingHeadsPlugin {
  constructor(api) {
    this.api = api;
    this.logger = api.logger;
    this.io = api.getSocketIO();
    this.db = api.getDatabase();
    
    // Load configuration
    this.config = this._loadConfig();
    
    // Initialize managers and engines
    this.cacheManager = null;
    this.roleManager = null;
    this.avatarGenerator = null;
    this.spriteGenerator = null;
    this.animationController = null;
    this.activeImageProvider = 'siliconflow';
    this.logBuffer = [];
    this.maxLogEntries = 200;
    
    // TTS event tracking
    this.ttsEventQueue = [];
    this.processingTTS = false;
    
    // Custom voice users (loaded from TTS plugin config)
    this.customVoiceUsers = [];

    // Bridge handlers for TTS playback events
    this.ttsBridgeHandlers = null;
  }

  /**
   * Load plugin configuration from database
   * @returns {object} Configuration object
   * @private
   */
  _loadConfig() {
    const defaultConfig = {
      enabled: false,
      imageApiUrl: 'https://api.siliconflow.com/v1/images/generations',
      imageProvider: 'auto',
      openaiImageModel: 'dall-e-3',
      defaultStyle: 'cartoon',
      cacheEnabled: true,
      cacheDuration: 2592000000, // 30 days in milliseconds
      obsEnabled: true,
      obsHudEnabled: true,
      spawnAnimationMode: 'standard',
      spawnAnimationUrl: '',
      spawnAnimationVolume: 0.8,
      animationDuration: 5000,
      fadeInDuration: 300,
      fadeOutDuration: 300,
      blinkInterval: 3000,
      rolePermission: 'all',
      minTeamLevel: 0,
      requireSubscriber: false,
      requireCustomVoice: false,
      avatarResolution: 1500,
      spriteResolution: 512,
      debugLogging: false // Enable/disable detailed logging
    };

    const savedConfig = this.api.getConfig('talking_heads_config');
    const mergedConfig = savedConfig ? { ...defaultConfig, ...savedConfig } : defaultConfig;
    return {
      ...mergedConfig,
      imageApiUrl: this._normalizeImageApiUrl(
        mergedConfig.imageApiUrl,
        defaultConfig.imageApiUrl
      )
    };
  }

  /**
   * Normalize legacy SiliconFlow image API URLs
   * - Switches deprecated .cn domain to .com
   * - Fixes singular /image/ path to /images/
   * @param {string} url - Configured URL
   * @param {string} fallback - Fallback URL if normalization fails
   * @returns {string} Normalized URL
   * @private
   */
  _normalizeImageApiUrl(url, fallback = null) {
    const safeDefault = 'https://api.siliconflow.com/v1/images/generations';
    if (!url) return fallback || safeDefault;

    try {
      const parsed = new URL(url.trim());
      if (parsed.hostname === 'api.siliconflow.cn') {
        parsed.hostname = 'api.siliconflow.com';
      }
      if (parsed.pathname === '/v1/image/generations') {
        parsed.pathname = '/v1/images/generations';
      }
      return parsed.toString();
    } catch (error) {
      this._log(`Failed to normalize imageApiUrl: ${error.message}`, 'warn');
      return fallback || safeDefault;
    }
  }

  /**
   * Get SiliconFlow API key from global settings
   * @returns {string|null} SiliconFlow API key
   * @private
   */
  _getSiliconFlowApiKey() {
    try {
      // Try global settings (centralized key)
      const key = this.db.getSetting('siliconflow_api_key') || 
                 this.db.getSetting('streamalchemy_siliconflow_api_key');
      if (key) {
        this._log(`Found SiliconFlow API key in database`, 'debug');
        return key;
      }
    } catch (error) {
      this._log(`Failed to get SiliconFlow key from settings: ${error.message}`, 'warn');
    }
    // Try environment variable
    const envKey = process.env.SILICONFLOW_API_KEY || null;
    if (envKey) {
      this._log(`Found SiliconFlow API key in environment`, 'debug');
    }
    return envKey;
  }

  /**
   * Get OpenAI API key from global settings or environment
   * @returns {string|null} OpenAI API key
   * @private
   */
  _getOpenAIApiKey() {
    try {
      const key = this.db.getSetting('openai_api_key') || this.db.getSetting('tts_openai_api_key');
      if (key) {
        this._log('Found OpenAI API key in database', 'debug');
        return key;
      }
    } catch (error) {
      this._log(`Failed to get OpenAI key from settings: ${error.message}`, 'warn');
    }

    const envKey = process.env.OPENAI_API_KEY || null;
    if (envKey) {
      this._log('Found OpenAI API key in environment', 'debug');
    }
    return envKey;
  }

  /**
   * Resolve which image provider and API key to use
   * @returns {{provider: string, apiKey: string|null, apiKeySource: string}}
   * @private
   */
  _resolveImageProvider() {
    const preference = this.config.imageProvider || 'auto';
    const openaiKey = this._getOpenAIApiKey();
    const siliconKey = this._getSiliconFlowApiKey();

    if (preference === 'openai') {
      return {
        provider: 'openai',
        apiKey: openaiKey,
        apiKeySource: openaiKey ? 'openai_settings' : 'none'
      };
    }

    if (preference === 'siliconflow') {
      return {
        provider: 'siliconflow',
        apiKey: siliconKey,
        apiKeySource: siliconKey ? 'global_settings' : 'none'
      };
    }

    if (openaiKey) {
      return { provider: 'openai', apiKey: openaiKey, apiKeySource: 'openai_settings' };
    }

    return { provider: 'siliconflow', apiKey: siliconKey, apiKeySource: siliconKey ? 'global_settings' : 'none' };
  }

  /**
   * Initialize or re-initialize avatar and sprite generators with current API key
   * @returns {boolean} True if generators were successfully initialized
   * @private
   */
  _initializeGenerators() {
    const { provider, apiKey } = this._resolveImageProvider();
    
    if (!apiKey) {
      this._log(`⚠️  No API key configured for ${provider} - avatar generation disabled`, 'warn');
      if (provider === 'openai') {
        this._log('Configure the API key in Dashboard > Settings > OpenAI API Configuration', 'info');
      } else {
        this._log('Configure the API key in Dashboard > Settings > TTS API Keys > SiliconFlow API Key', 'info');
      }
      return false;
    }

    this._log('Initializing AI engines...', 'debug');
    this.activeImageProvider = provider;
    
    this.avatarGenerator = new AvatarGenerator(
      provider === 'openai' ? 'https://api.openai.com/v1/images/generations' : this.config.imageApiUrl,
      apiKey,
      this.logger,
      { ...this.config, imageProvider: provider }
    );

    this.spriteGenerator = new SpriteGenerator(
      provider === 'openai' ? 'https://api.openai.com/v1/images/generations' : this.config.imageApiUrl,
      apiKey,
      this.logger,
      { ...this.config, imageProvider: provider }
    );

    this._log(`✅ Avatar and sprite generators initialized (${provider})`, 'info');
    return true;
  }

  /**
   * Log message with debug level control
   * @param {string} message - Log message
   * @param {string} level - Log level (info, warn, error, debug)
   * @param {object} data - Additional data to log
   * @private
   */
  _log(message, level = 'info', data = null) {
    const prefix = 'TalkingHeads:';
    const fullMessage = `${prefix} ${message}`;
    const entry = {
      level,
      message: fullMessage,
      data: data || null,
      timestamp: new Date().toISOString()
    };

    // Safety check
    if (!this.logger) {
      this._appendLogEntry(entry);
      console.warn('TalkingHeads: Logger not initialized');
      return;
    }
    
    // Always log errors and warnings
    if (level === 'error' || level === 'warn') {
      this._appendLogEntry(entry);
      this.logger[level](fullMessage, data || '');
      return;
    }
    
    // Log info and debug based on debugLogging setting
    // Default to false if config or debugLogging is undefined
    const debugEnabled = this.config && this.config.debugLogging === true;
    if (level === 'debug' && !debugEnabled) {
      return; // Skip debug logs if debugging is disabled
    }
    
    this._appendLogEntry(entry);
    if (data) {
      this.logger[level](fullMessage, data);
    } else {
      this.logger[level](fullMessage);
    }
  }

  /**
   * Append log entry to in-memory buffer for UI consumption
   * @param {{level: string, message: string, data?: object, timestamp: string}} entry
   * @private
   */
  _appendLogEntry(entry) {
    this.logBuffer.push(entry);
    if (this.logBuffer.length > this.maxLogEntries) {
      this.logBuffer.shift();
    }
  }

  /**
   * Convert absolute sprite paths to relative URLs for overlays/HUD
   * @param {object} sprites
   * @returns {object}
   * @private
   */
  _getRelativeSpritePaths(sprites) {
    const relativeSprites = {};
    Object.entries(sprites || {}).forEach(([key, value]) => {
      if (value) {
        const filename = value.split(/[\\/]/).pop();
        relativeSprites[key] = `/api/talkingheads/sprite/${filename}`;
      }
    });
    return relativeSprites;
  }

  /**
   * Emit spawn animation event for new avatars
   * @param {string} userId
   * @param {string} username
   * @param {object} sprites
   * @private
   */
  _emitSpawnAnimation(userId, username, sprites) {
    const volume = typeof this.config.spawnAnimationVolume === 'number'
      ? Math.min(1, Math.max(0, this.config.spawnAnimationVolume))
      : 0.8;

    this.io.emit('talkingheads:avatar:spawn', {
      userId,
      username,
      sprites: this._getRelativeSpritePaths(sprites),
      mode: this.config.spawnAnimationMode || 'standard',
      customMediaUrl: this.config.spawnAnimationUrl || '',
      volume
    });
  }

  /**
   * Return recent log entries
   * @param {number} limit
   * @returns {Array}
   * @private
   */
  _getRecentLogs(limit = 100) {
    const startIndex = Math.max(0, this.logBuffer.length - limit);
    return this.logBuffer.slice(startIndex);
  }

  /**
   * Save configuration to database
   * @param {object} newConfig - Configuration to save
   * @private
   */
  _saveConfig(newConfig) {
    try {
      const oldDebugLogging = this.config ? this.config.debugLogging : false;
      this.config = { ...this.config, ...newConfig };
      this.config.spawnAnimationMode = this.config.spawnAnimationMode || 'standard';
      this.config.spawnAnimationUrl = this.config.spawnAnimationUrl || '';
      if (typeof this.config.spawnAnimationVolume === 'number') {
        this.config.spawnAnimationVolume = Math.min(1, Math.max(0, this.config.spawnAnimationVolume));
      } else {
        this.config.spawnAnimationVolume = 0.8;
      }
      this.api.setConfig('talking_heads_config', this.config);
      
      // Log configuration change
      this._log('Configuration saved', 'info');
      
      // If debug logging was toggled, log the change
      if (oldDebugLogging !== this.config.debugLogging) {
        this._log(`Debug logging ${this.config.debugLogging ? 'ENABLED' : 'DISABLED'}`, 'info');
      }
      
      // Log other important config changes
      if (this.config.debugLogging) {
        this._log('Config updated', 'debug', {
          enabled: this.config.enabled,
          defaultStyle: this.config.defaultStyle,
          rolePermission: this.config.rolePermission,
          cacheEnabled: this.config.cacheEnabled,
          debugLogging: this.config.debugLogging
        });
      }
    } catch (error) {
      // If logging fails, use basic logger
      if (this.logger) {
        this.logger.error('TalkingHeads: Failed to save config internally', error);
      }
      throw error; // Re-throw to be caught by the route handler
    }
  }

  /**
   * Initialize plugin
   */
  async init() {
    try {
      this._log('Initializing plugin...', 'info');
      this._log(`Debug logging: ${this.config.debugLogging ? 'ENABLED' : 'DISABLED'}`, 'info');

      // Ensure plugin data directory exists
      const pluginDataDir = this.api.getPluginDataDir();
      this._log(`Plugin data directory: ${pluginDataDir}`, 'debug');
      await this.api.ensurePluginDataDir();

      // Initialize cache manager
      this._log('Initializing cache manager...', 'debug');
      this.cacheManager = new CacheManager(pluginDataDir, this.db, this.logger, this.config);
      await this.cacheManager.init();
      this._log('Cache manager initialized', 'debug');

      // Initialize role manager
      this._log('Initializing role manager...', 'debug');
      this.roleManager = new RoleManager(this.config, this.logger);
      this._log(`Role permission: ${this.config.rolePermission}`, 'debug');

      // Initialize avatar and sprite generators if API key is configured
      this._initializeGenerators();

      // Initialize animation controller
      this._log('Initializing animation controller...', 'debug');
      this.animationController = new AnimationController(
        this.io,
        this.logger,
        this.config,
        null // OBS WebSocket integration can be added later
      );
      this._log('Animation controller initialized', 'debug');

    // Register API routes
    this._log('Registering API routes...', 'debug');
    this._registerRoutes();

    // Register socket events
      this._log('Registering socket events...', 'debug');
      this._registerSocketEvents();

    // Register TTS event listener
    this._log('Registering TTS event listeners...', 'debug');
    this._registerTTSEvents();

    // Bridge playback events from TTS plugin so avatars follow speech
    this._registerPlaybackBridge();

    // Load custom voice users from TTS plugin
    this._log('Loading custom voice users...', 'debug');
    this._loadCustomVoiceUsers();

      // Start cache cleanup interval (once per day)
      this._startCacheCleanup();

      this.logger.info('TalkingHeads: ✅ Plugin initialized successfully');

    } catch (error) {
      this.logger.error('TalkingHeads: Failed to initialize plugin', error);
      throw error;
    }
  }

  /**
   * Register API routes
   * @private
   */
  _registerRoutes() {
    // Serve overlay and OBS HUD
    this.api.registerRoute('get', '/talking-heads/overlay', (req, res) => {
      res.sendFile(path.join(__dirname, 'overlay.html'));
    });

    this.api.registerRoute('get', '/talking-heads/obs-hud', (req, res) => {
      res.sendFile(path.join(__dirname, 'obs-hud.html'));
    });

    const assetsDir = path.join(__dirname, 'assets');
    this.api.registerRoute('get', '/talking-heads/assets/:filename', (req, res) => {
      const safeFilename = path.basename(req.params.filename || '');
      res.sendFile(path.join(assetsDir, safeFilename));
    });

    // OBS overlay aliases (stream overlay namespace)
    this.api.registerRoute('get', '/overlay/talking-heads', (req, res) => {
      res.sendFile(path.join(__dirname, 'overlay.html'));
    });

    this.api.registerRoute('get', '/overlay/talking-heads/obs-hud', (req, res) => {
      res.sendFile(path.join(__dirname, 'obs-hud.html'));
    });

    this.api.registerRoute('get', '/overlay/talking-heads/assets/:filename', (req, res) => {
      const safeFilename = path.basename(req.params.filename || '');
      res.sendFile(path.join(assetsDir, safeFilename));
    });

    // Get configuration
    this.api.registerRoute('get', '/api/talkingheads/config', (req, res) => {
      const providerInfo = this._resolveImageProvider();
      res.json({
        success: true,
        config: this.config,
        styleTemplates: getAllStyleTemplates(),
        apiConfigured: !!providerInfo.apiKey,
        apiKeySource: providerInfo.apiKeySource,
        provider: providerInfo.provider
      });
    });

    // Update configuration
    this.api.registerRoute('post', '/api/talkingheads/config', (req, res) => {
      try {
        // Shallow copy to avoid mutating Express request body
        const newConfig = { ...req.body };
        
        // Remove imageApiKey if sent (should not be stored here)
        if (newConfig.imageApiKey !== undefined) {
          delete newConfig.imageApiKey;
        }

        if (newConfig.spawnAnimationVolume !== undefined) {
          newConfig.spawnAnimationVolume = parseFloat(newConfig.spawnAnimationVolume);
        }
        
        this._saveConfig(newConfig);

        // Update managers with new config
        if (this.roleManager) {
          this.roleManager.updateConfig(this.config);
        }

        const apiKey = this._resolveImageProvider().apiKey;
        res.json({ 
          success: true, 
          config: this.config,
          apiConfigured: !!apiKey
        });
      } catch (error) {
        this.logger.error('TalkingHeads: Failed to save config', error);
        res.status(500).json({ success: false, error: error.message });
      }
    });

    // Get cache statistics
    this.api.registerRoute('get', '/api/talkingheads/cache/stats', (req, res) => {
      try {
        const stats = this.cacheManager.getStats();
        res.json({ success: true, stats });
      } catch (error) {
        res.status(500).json({ success: false, error: error.message });
      }
    });

    this.api.registerRoute('get', '/api/talkingheads/cache/list', (req, res) => {
      try {
        const limit = parseInt(req.query.limit, 10) || 50;
        const entries = this.cacheManager.listAvatars(limit);
        const avatars = entries.map((entry) => ({
          ...entry,
          sprites: this._getRelativeSpritePaths(entry.sprites)
        }));
        res.json({ success: true, avatars });
      } catch (error) {
        res.status(500).json({ success: false, error: error.message });
      }
    });

    // Clear cache
    this.api.registerRoute('post', '/api/talkingheads/cache/clear', async (req, res) => {
      try {
        const deleted = await this.cacheManager.clearAllCache();
        res.json({ success: true, deleted });
      } catch (error) {
        res.status(500).json({ success: false, error: error.message });
      }
    });

    // Test API connection
    this.api.registerRoute('post', '/api/talkingheads/test-api', async (req, res) => {
      try {
        const providerInfo = this._resolveImageProvider();
        // Try to initialize generators if they don't exist yet
        if (!this.avatarGenerator) {
          this._log('Avatar generator not initialized, attempting to initialize...', 'debug');
          const initialized = this._initializeGenerators();
          
          if (!initialized) {
            return res.json({ 
              success: false, 
              error: providerInfo.provider === 'openai'
                ? 'API key not configured. Please configure the OpenAI API key in Dashboard > Settings > OpenAI API Configuration and reload the plugin.'
                : 'API key not configured. Please configure the SiliconFlow API key in Dashboard > Settings > TTS API Keys and reload the plugin.'
            });
          }
        }

        const connected = await this.avatarGenerator.testConnection();
        
        if (connected) {
          this._log('API connection test successful', 'info');
        } else {
          this._log('API connection test failed', 'warn');
        }
        
        res.json({ 
          success: connected, 
          message: connected ? 'API connection successful' : 'API connection failed - check API key and network connection',
          provider: this.activeImageProvider
        });
      } catch (error) {
        this._log(`Test API error: ${error.message}`, 'error');
        res.status(500).json({ success: false, error: error.message || 'Unknown error' });
      }
    });

    // Test avatar generation
    this.api.registerRoute('post', '/api/talkingheads/test-generate', async (req, res) => {
      try {
        const providerInfo = this._resolveImageProvider();
        // Try to initialize generators if they don't exist yet
        if (!this.avatarGenerator || !this.spriteGenerator) {
          this._log('Generators not initialized, attempting to initialize...', 'debug');
          const initialized = this._initializeGenerators();
          
          if (!initialized) {
            return res.json({ 
              success: false, 
              error: providerInfo.provider === 'openai'
                ? 'API key not configured. Please configure the OpenAI API key in Dashboard > Settings > OpenAI API Configuration and reload the plugin.'
                : 'API key not configured. Please configure the SiliconFlow API key in Dashboard > Settings > TTS API Keys and reload the plugin.'
            });
          }
        }

        const { styleKey } = req.body;
        const style = styleKey || this.config.defaultStyle;
        
        this._log(`Testing avatar generation with style: ${style}`, 'info');

        // Generate a test avatar with dummy user data
        const testUserId = `test_${Date.now()}`;
        const testUsername = 'TestUser';
        
        const result = await this._generateAvatarAndSprites(
          testUserId,
          testUsername,
          '',
          style
        );

        res.json({ 
          success: true, 
          message: 'Test avatar generated successfully',
          sprites: result.sprites ? Object.keys(result.sprites).length : 0,
          cacheId: result.cacheId
        });
      } catch (error) {
        this._log(`Test generation failed: ${error.message}`, 'error');
        res.status(500).json({ success: false, error: error.message || 'Generation failed' });
      }
    });

    // Talking Heads + TTS preview using local engine
    this.api.registerRoute('post', '/api/talkingheads/preview-tts', async (req, res) => {
      try {
        const ttsPlugin = this.api.pluginLoader?.getPluginInstance('tts');
        if (!ttsPlugin || typeof ttsPlugin.speak !== 'function') {
          return res.status(503).json({ success: false, error: 'TTS plugin is not available' });
        }

        const previewText = (req.body && req.body.text) || 'Hallo! Dies ist eine Talking Heads Vorschau.';
        const previewUserId = (req.body && req.body.userId) || 'talkingheads_preview';
        const previewUsername = (req.body && req.body.username) || 'TalkingHeads Preview';

        // Ensure avatar exists before playback to avoid cold start delays
        const cached = this.cacheManager.getAvatar(previewUserId, this.config.defaultStyle);
        if (!cached && (!this.avatarGenerator || !this.spriteGenerator)) {
          this._initializeGenerators();
        }
        if (!cached && this.avatarGenerator && this.spriteGenerator) {
          try {
            await this._generateAvatarAndSprites(
              previewUserId,
              previewUsername,
              '',
              this.config.defaultStyle
            );
          } catch (genError) {
            this._log(`Preview avatar generation failed: ${genError.message}`, 'warn');
          }
        }

        const speakResult = await ttsPlugin.speak({
          text: previewText,
          userId: previewUserId,
          username: previewUsername,
          source: 'talking-heads-preview',
          engine: ttsPlugin.config?.defaultEngine || undefined,
          priority: 0
        });

        res.json({ success: true, result: speakResult });
      } catch (error) {
        this.logger.error('TalkingHeads: Preview TTS failed', error);
        res.status(500).json({ success: false, error: error.message || 'Preview failed' });
      }
    });

    // Manually generate avatar for user
    this.api.registerRoute('post', '/api/talkingheads/generate', async (req, res) => {
      try {
        const { userId, username, styleKey, profileImageUrl } = req.body;

        if (!userId || !username) {
          return res.status(400).json({ success: false, error: 'Missing required fields' });
        }

        const result = await this._generateAvatarAndSprites(
          userId,
          username,
          profileImageUrl || '',
          styleKey || this.config.defaultStyle
        );

        res.json({ success: true, result });
      } catch (error) {
        this.logger.error('TalkingHeads: Manual generation failed', error);
        res.status(500).json({ success: false, error: error.message });
      }
    });

    // Get users from stream database for assignment
    this.api.registerRoute('get', '/api/talkingheads/users', (req, res) => {
      try {
        // Validate and limit the number of users to prevent DoS
        const requestedLimit = parseInt(req.query.limit, 10);
        const limit = Number.isNaN(requestedLimit) 
          ? 100 
          : Math.min(Math.max(requestedLimit, 1), 1000);
        
        const users = this.db.getAllUserStatistics(limit, 0);
        
        // Map users with talking head status
        const usersWithStatus = users.map(user => {
          const hasAvatar = this.cacheManager.hasAvatar(user.user_id, this.config.defaultStyle);
          const cached = hasAvatar ? this.cacheManager.getAvatar(user.user_id, this.config.defaultStyle) : null;
          
          return {
            userId: user.user_id,
            username: user.username,
            uniqueId: user.unique_id,
            profilePictureUrl: user.profile_picture_url,
            totalCoins: user.total_coins_sent,
            totalGifts: user.total_gifts_sent,
            totalComments: user.total_comments,
            lastSeenAt: user.last_seen_at,
            hasAvatar,
            avatarCreatedAt: cached ? cached.createdAt : null,
            avatarStyleKey: cached ? cached.styleKey : null
          };
        });
        
        res.json({ success: true, users: usersWithStatus });
      } catch (error) {
        this.logger.error('TalkingHeads: Failed to fetch users', error);
        res.status(500).json({ success: false, error: error.message });
      }
    });

    // Assign/generate talking head for user with LLM-based profile analysis
    this.api.registerRoute('post', '/api/talkingheads/assign', async (req, res) => {
      try {
        const { userId, username, profileImageUrl, styleKey } = req.body;

        if (!userId || !username) {
          return res.status(400).json({ success: false, error: 'Missing required fields: userId and username' });
        }

        // Validate profile image URL if provided
        if (profileImageUrl) {
          try {
            const url = new URL(profileImageUrl);
            if (!['http:', 'https:'].includes(url.protocol)) {
              return res.status(400).json({ 
                success: false, 
                error: 'Invalid profile image URL: Only HTTP/HTTPS URLs are allowed' 
              });
            }
          } catch (error) {
            return res.status(400).json({ 
              success: false, 
              error: 'Invalid profile image URL format' 
            });
          }
        }

        const style = styleKey || this.config.defaultStyle;

        // Step 1: Analyze profile image and username with LLM (if available and profile image exists)
        let avatarDescription = null;
        let llmFallbackReason = null;
        
        if (profileImageUrl && this._getOpenAIApiKey()) {
          try {
            this._log(`Analyzing profile for user ${username} with LLM...`, 'info');
            avatarDescription = await this._analyzeProfileWithLLM(username, profileImageUrl, style);
            this._log(`LLM analysis complete for ${username}: ${avatarDescription}`, 'debug');
          } catch (error) {
            llmFallbackReason = error.message;
            this._log(`LLM analysis failed for ${username}: ${error.message}. Using default prompt.`, 'warn');
            avatarDescription = null;
          }
        } else if (!profileImageUrl) {
          llmFallbackReason = 'No profile image URL provided';
        } else {
          llmFallbackReason = 'OpenAI API key not configured';
        }

        // Step 2: Generate avatar and sprites
        const result = await this._generateAvatarAndSprites(
          userId,
          username,
          profileImageUrl || '',
          style,
          avatarDescription // Pass the LLM-generated description as override
        );

        res.json({ 
          success: true, 
          result,
          llmAnalysisUsed: !!avatarDescription,
          llmFallbackReason: avatarDescription ? null : llmFallbackReason
        });
      } catch (error) {
        this.logger.error('TalkingHeads: User assignment failed', error);
        res.status(500).json({ success: false, error: error.message });
      }
    });

    // Get active animations
    this.api.registerRoute('get', '/api/talkingheads/animations', (req, res) => {
      try {
        const animations = this.animationController.getActiveAnimations();
        res.json({ success: true, animations });
      } catch (error) {
        res.status(500).json({ success: false, error: error.message });
      }
    });

    // Expose recent logs for the admin UI
    this.api.registerRoute('get', '/api/talkingheads/logs', (req, res) => {
      try {
        const limit = parseInt(req.query.limit, 10) || 100;
        res.json({ success: true, logs: this._getRecentLogs(limit) });
      } catch (error) {
        res.status(500).json({ success: false, error: error.message });
      }
    });

    // Serve sprite images
    this.api.registerRoute('get', '/api/talkingheads/sprite/:filename', async (req, res) => {
      try {
        const filename = req.params.filename;
        const pluginDataDir = this.api.getPluginDataDir();
        const filepath = path.join(pluginDataDir, 'avatars', filename);

        // Check if file exists
        await fs.access(filepath);
        
        // Send file
        res.sendFile(filepath);
      } catch (error) {
        res.status(404).json({ success: false, error: 'Sprite not found' });
      }
    });

    this.logger.info('TalkingHeads: API routes registered');
  }

  /**
   * Register socket events
   * @private
   */
  _registerSocketEvents() {
    // Client requests animation test
    this.api.registerSocket('talkingheads:test', async (data) => {
      try {
        const { userId, username, duration } = data;
        
        // Get or generate avatar
        const cached = this.cacheManager.getAvatar(userId, this.config.defaultStyle);
        
        if (cached) {
          this.animationController.startAnimation(
            userId,
            username,
            cached.sprites,
            duration || 5000
          );
        } else {
          this.logger.warn('TalkingHeads: No cached avatar for test animation');
        }
      } catch (error) {
        this.logger.error('TalkingHeads: Test animation failed', error);
      }
    });

    this.logger.info('TalkingHeads: Socket events registered');
  }

  /**
   * Register TTS event listeners
   * @private
   */
  _registerTTSEvents() {
    // Listen for TTS events from TTS plugin
    this.io.on('connection', (socket) => {
      socket.on('tts:speaking', async (data) => {
        if (!this.config.enabled) return;

        try {
          await this._handleTTSEvent(data);
        } catch (error) {
          this.logger.error('TalkingHeads: Failed to handle TTS event', error);
        }
      });
    });

    this.logger.info('TalkingHeads: TTS event listeners registered');
  }

  /**
   * Bridge TTS playback events from the TTS plugin to Talking Heads animations
   * Uses PluginLoader event emitter to avoid socket roundtrips
   * @private
   */
  _registerPlaybackBridge() {
    const loader = this.api.pluginLoader;
    if (!loader || typeof loader.on !== 'function') {
      this._log('PluginLoader not available for TTS bridge', 'debug');
      return;
    }

    const startHandler = async (payload = {}) => {
      if (!this.config.enabled) return;
      const userId = payload.userId || payload.username;
      if (!userId) return;

      try {
        await this._handleTTSEvent({
          userId,
          username: payload.username || userId,
          text: payload.text || '',
          duration: payload.duration || this.config.animationDuration || 5000,
          userData: {
            profilePictureUrl: payload.profileImageUrl || '',
            uniqueId: userId,
            hasAssignedVoice: payload.hasAssignedVoice === true
          }
        });
      } catch (error) {
        this.logger.error('TalkingHeads: Failed to handle bridged TTS playback', error);
      }
    };

    const endHandler = (payload = {}) => {
      const userId = payload.userId || payload.username;
      if (!userId || !this.animationController) return;
      this.animationController.stopAnimation(userId);
    };

    loader.on('tts:playback:started', startHandler);
    loader.on('tts:playback:ended', endHandler);
    this.ttsBridgeHandlers = { startHandler, endHandler };
    this._log('TTS playback bridge registered', 'debug');
  }

  /**
   * Handle TTS speaking event
   * @param {object} data - TTS event data
   * @private
   */
  async _handleTTSEvent(data) {
    const { userId, username, text, duration, userData } = data;

    this._log(`TTS event received for user: ${username}`, 'debug', { userId, duration });

    if (!userId || !username) {
      this._log('Invalid TTS event data - missing userId or username', 'warn');
      return;
    }

    const enrichedUserData = {
      ...(userData || {}),
      uniqueId: userId || username,
      username,
      hasAssignedVoice: userData?.hasAssignedVoice === true
    };

    // Check role permission
    this._log(`Checking eligibility for user: ${username}`, 'debug');
    const eligibility = this.roleManager.checkEligibility(enrichedUserData, this.customVoiceUsers);
    
    if (!eligibility.eligible) {
      this._log(`User ${username} not eligible - ${eligibility.reason}`, 'info');
      return;
    }

    this._log(`User ${username} is eligible for talking head`, 'debug');

    // Check cache first
    this._log(`Checking cache for user ${username} with style ${this.config.defaultStyle}`, 'debug');
    let avatarData = this.cacheManager.getAvatar(userId, this.config.defaultStyle);
    const wasCached = !!avatarData;

    if (!avatarData) {
      // Generate new avatar and sprites
      this._log(`Generating new avatar for ${username}`, 'info');
      this._log(`Profile URL: ${enrichedUserData.profilePictureUrl || 'none'}`, 'debug');
      
      try {
        avatarData = await this._generateAvatarAndSprites(
          userId,
          username,
          enrichedUserData.profilePictureUrl || '',
          this.config.defaultStyle
        );
        this._log(`Avatar generation completed for ${username}`, 'debug');
      } catch (error) {
        this._log(`Failed to generate avatar for ${username}: ${error.message}`, 'error');
        return;
      }
    } else {
      this._log(`Using cached avatar for ${username}`, 'debug');
    }

    const isNewAvatar = !wasCached;

    // Start animation
    this._log(`Starting animation for ${username} (duration: ${duration}ms)`, 'debug');
    if (isNewAvatar && this.config.obsHudEnabled !== false) {
      this._emitSpawnAnimation(userId, username, avatarData.sprites);
    }
    this.animationController.startAnimation(
      userId,
      username,
      avatarData.sprites,
      duration || 5000
    );
  }

  /**
   * Generate avatar and sprites for user
   * @param {string} userId - TikTok user ID
   * @param {string} username - TikTok username
   * @param {string} profileImageUrl - Profile image URL
   * @param {string} styleKey - Style template key
   * @param {string} customDescription - Optional LLM-generated custom description
   * @returns {Promise<object>} Avatar data
   * @private
   */
  async _generateAvatarAndSprites(userId, username, profileImageUrl, styleKey, customDescription = null) {
    if (!this.avatarGenerator || !this.spriteGenerator) {
      throw new Error('Avatar generation not configured - API key missing');
    }

    const pluginDataDir = this.api.getPluginDataDir();
    const cacheDir = path.join(pluginDataDir, 'avatars');

    // Generate avatar with optional custom description
    const avatarPath = await this.avatarGenerator.generateAvatar(
      username,
      userId,
      profileImageUrl,
      styleKey,
      cacheDir,
      customDescription
    );

    // Generate sprites
    const spritePaths = await this.spriteGenerator.generateSprites(
      username,
      userId,
      avatarPath,
      styleKey,
      cacheDir
    );

    // Save to cache
    this.cacheManager.saveAvatar(
      userId,
      username,
      styleKey,
      avatarPath,
      spritePaths,
      profileImageUrl
    );

    return {
      userId,
      username,
      styleKey,
      avatarPath,
      sprites: spritePaths
    };
  }

  /**
   * Analyze user profile with LLM to generate avatar description
   * Uses OpenAI's vision API to analyze profile image and generate description
   * @param {string} username - TikTok username
   * @param {string} profileImageUrl - URL to profile image
   * @param {string} styleKey - Style template key to match genre
   * @returns {Promise<string>} LLM-generated avatar description
   * @private
   */
  async _analyzeProfileWithLLM(username, profileImageUrl, styleKey) {
    const OpenAI = require('openai');
    const apiKey = this._getOpenAIApiKey();
    
    if (!apiKey) {
      throw new Error('OpenAI API key not configured');
    }
    
    const client = new OpenAI({ apiKey });
    const { getStyleTemplate } = require('./utils/style-templates');
    const styleTemplate = getStyleTemplate(styleKey);
    
    if (!styleTemplate) {
      throw new Error(`Invalid style key: ${styleKey}`);
    }

    try {
      const prompt = `You are an expert character designer. Analyze the profile image and username to create a detailed character description for a 2D avatar.

Username: ${username}
Style Genre: ${styleTemplate.name} (${styleTemplate.description})

Based on the profile image and username, describe a unique 2D avatar character that:
1. Captures the essence and personality suggested by the profile image
2. Fits the ${styleTemplate.name} art style (${styleTemplate.description})
3. Works well for a TikTok livestream talking head animation
4. Has clear, expressive facial features suitable for lip-sync animation

Provide a detailed character description including:
- Physical appearance (face, hair, clothing, colors)
- Personality traits reflected in the design
- Key visual elements that make this character unique
- How it fits the ${styleTemplate.name} style

Keep the description focused and specific. This will be used to generate the actual avatar image.`;

      this._log(`Sending LLM request for profile analysis (${username})`, 'debug');

      const response = await client.chat.completions.create({
        model: 'gpt-4o-mini',
        messages: [
          {
            role: 'user',
            content: [
              {
                type: 'text',
                text: prompt
              },
              {
                type: 'image_url',
                image_url: {
                  url: profileImageUrl
                }
              }
            ]
          }
        ],
        max_tokens: 500,
        // Temperature 0.7: Balanced between creativity and consistency
        // High enough for diverse character descriptions, low enough to stay on-topic
        temperature: 0.7
      });

      if (!response.choices || response.choices.length === 0) {
        throw new Error('No response from OpenAI');
      }

      const description = response.choices[0].message.content.trim();
      this._log(`LLM generated description (${description.length} chars)`, 'debug');
      
      return description;
    } catch (error) {
      this._log(`LLM analysis failed: ${error.message}`, 'error');
      throw error;
    }
  }

  /**
   * Load custom voice users from TTS plugin
   * @private
   */
  _loadCustomVoiceUsers() {
    try {
      // Try to get TTS plugin config
      const ttsConfig = this.api.getConfig('tts_config');
      
      if (ttsConfig && ttsConfig.voiceWhitelist) {
        this.customVoiceUsers = Object.keys(ttsConfig.voiceWhitelist);
        this.logger.info(`TalkingHeads: Loaded ${this.customVoiceUsers.length} custom voice users`);
      }
    } catch (error) {
      this.logger.warn('TalkingHeads: Could not load custom voice users', error);
    }
  }

  /**
   * Start cache cleanup interval
   * @private
   */
  _startCacheCleanup() {
    if (!this.config.cacheEnabled) return;

    // Run cleanup once per day
    this.cacheCleanupInterval = setInterval(async () => {
      try {
        await this.cacheManager.cleanupOldCache();
      } catch (error) {
        this.logger.error('TalkingHeads: Cache cleanup failed', error);
      }
    }, 86400000); // 24 hours

    this.logger.info('TalkingHeads: Cache cleanup scheduled');
  }

  /**
   * Destroy plugin and cleanup
   */
  async destroy() {
    try {
      this.logger.info('TalkingHeads: Destroying plugin...');

      // Stop all animations
      if (this.animationController) {
        this.animationController.stopAllAnimations();
      }

      if (this.ttsBridgeHandlers && this.api.pluginLoader) {
        const loader = this.api.pluginLoader;
        loader.removeListener('tts:playback:started', this.ttsBridgeHandlers.startHandler);
        loader.removeListener('tts:playback:ended', this.ttsBridgeHandlers.endHandler);
        this.ttsBridgeHandlers = null;
      }

      // Clear cleanup interval
      if (this.cacheCleanupInterval) {
        clearInterval(this.cacheCleanupInterval);
      }

      this.logger.info('TalkingHeads: Plugin destroyed');
    } catch (error) {
      this.logger.error('TalkingHeads: Error during destroy', error);
    }
  }
}

module.exports = TalkingHeadsPlugin;
