import { WebSocket, WebSocketServer } from 'ws';
import { Server } from 'http';
import { elevenLabsService } from './elevenLabsService';
import { BotAccessService } from './botAccessService';
import { ConversationService } from './conversationService';
import { prisma } from '../index';
import logger from '../utils/logger';
import { QuotaService } from './quotaService';
import { systemConfigService } from './systemConfigService';

// F1 — farewell warning: how many seconds before the hard stop the agent gets
// a one-shot heads up (shared between the bot timer and the quota timer), and
// how much extra time is granted after the countdown hits 0 so it can finish
// the goodbye sentence instead of being cut mid-word.
const FAREWELL_WARNING_SECONDS = 40;
const HARD_STOP_GRACE_SECONDS = 10;

interface ConnectionData {
  ws: WebSocket;
  agentId?: string;
  botId?: string;
  userId?: string;
  userRole?: string;
  conversationId?: string; // Database conversation ID
  sessionId?: string; // Current session ID for tracking actual conversation time
  conversation?: any; // ElevenLabs conversation instance
  usageInterval?: NodeJS.Timeout; // Interval for periodic usage updates
  timerInterval?: NodeJS.Timeout; // Interval for timer updates (every second)
  startTime?: number; // When the timer started
  initialTotalUsage?: number; // Initial total usage when timer started
  maxUsageSeconds?: number; // Maximum usage allowed for this bot
  farewellWarningSent?: boolean; // One-shot flag shared by bot timer + quota timer
  quotaHardStopTimeout?: NodeJS.Timeout; // Grace-period timeout before closing on quota exhaustion
}

export class WebSocketService {
  private static instance: WebSocketService;
  private wss: WebSocketServer | null = null;
  private connections = new Map<string, ConnectionData>();

  private constructor() {
    logger.info('🔧 WebSocketService singleton created');
  }

  public static getInstance(): WebSocketService {
    if (!WebSocketService.instance) {
      logger.info('🏭 Creating WebSocketService singleton instance');
      WebSocketService.instance = new WebSocketService();
    }
    return WebSocketService.instance;
  }

  /**
   * Initialize WebSocket server
   */
  initialize(server: Server) {
    this.wss = new WebSocketServer({ 
      server,
      path: '/ws'
    });
    
    this.wss.on('connection', (ws: WebSocket, request) => {
      this.handleConnection(ws, request);
    });

    logger.info('✅ WebSocket server initialized');
  }

  /**
   * Handle new WebSocket connection
   */
  private handleConnection(ws: WebSocket, request: any) {
    const connectionId = Math.random().toString(36).substring(7);
    this.connections.set(connectionId, { ws });

    logger.info('🔌 New WebSocket connection', { connectionId });

    // Send welcome message
    ws.send(JSON.stringify({
      type: 'connected',
      connectionId,
      message: 'Connected to chat server'
    }));

    ws.on('message', (data) => {
      this.handleMessage(connectionId, ws, data);
    });

    ws.on('close', () => {
      this.handleConnectionClose(connectionId);
    });

    ws.on('error', (error) => {
      logger.error('❌ WebSocket error', { connectionId, error: error.message });
      this.handleConnectionClose(connectionId);
    });
  }

  /**
   * Handle connection close
   */
  private async handleConnectionClose(connectionId: string) {
    logger.info('🔌 WebSocket connection closed', { connectionId });
    
    const connectionData = this.connections.get(connectionId);
    
    // Clear usage tracking interval if exists
    if (connectionData?.usageInterval) {
      clearInterval(connectionData.usageInterval);
      logger.info('⏱️ Usage tracking interval cleared', { connectionId });
    }
    
    // Clear timer interval if exists
    if (connectionData?.timerInterval) {
      clearInterval(connectionData.timerInterval);
      logger.info('⏰ Timer interval cleared', { connectionId });
    }

    // Clear pending quota hard-stop grace timeout if exists
    if (connectionData?.quotaHardStopTimeout) {
      clearTimeout(connectionData.quotaHardStopTimeout);
    }

    // End ElevenLabs conversation if exists
    if (connectionData?.conversation) {
      try {
        connectionData.conversation.endSession();
        logger.info('🔌 ElevenLabs conversation ended', { connectionId });
      } catch (error) {
        logger.error('❌ Error ending ElevenLabs conversation', { connectionId, error: error.message });
      }
    }
    
    // End session if exists (close the current session without ending the conversation)
    if (connectionData?.sessionId && connectionData?.conversationId) {
      try {
        const now = new Date();
        const session = await prisma.conversationSession.findUnique({
          where: { id: connectionData.sessionId },
          select: { startedAt: true }
        });

        if (session) {
          const durationSeconds = Math.floor(
            (now.getTime() - session.startedAt.getTime()) / 1000
          );

          await prisma.conversationSession.update({
            where: { id: connectionData.sessionId },
            data: {
              endedAt: now,
              durationSeconds: durationSeconds
            }
          });

          logger.info('✅ Session ended', {
            connectionId,
            sessionId: connectionData.sessionId,
            conversationId: connectionData.conversationId,
            durationSeconds
          });
        }
      } catch (error) {
        logger.error('❌ Error ending session', {
          connectionId,
          sessionId: connectionData.sessionId,
          error: error.message
        });
      }
    }

    // End database conversation if exists (for students and admins)
    if (connectionData?.conversationId && connectionData?.userId && 
        (connectionData?.userRole === 'STUDENT' || connectionData?.userRole === 'ADMIN')) {
      try {
        await ConversationService.endConversation(
          connectionData.conversationId,
          connectionData.userId,
          connectionData.userRole as any
        );
        logger.info('✅ Database conversation ended', { 
          connectionId, 
          conversationId: connectionData.conversationId,
          userRole: connectionData.userRole
        });

        // NOTE: usage is already recorded inside ConversationService.endConversation()
        // (sums all sessions and writes to the quota ledger). A second write used to
        // happen here with a per-connection-only duration, which always collided with
        // the ledger's unique constraint on relatedConversationId — redundant and,
        // if 'close' and 'error' both fired for the same disconnect, a source of noise.
      } catch (error) {
        logger.error('❌ Error ending database conversation', {
          connectionId, 
          conversationId: connectionData.conversationId,
          userRole: connectionData.userRole,
          error: error.message 
        });
      }
    } else if (connectionData?.userRole && !['STUDENT', 'ADMIN'].includes(connectionData.userRole)) {
      logger.info('ℹ️ Skipping database conversation end for user role', {
        connectionId,
        userRole: connectionData?.userRole
      });
    }
    
    // Remove connection from map
    this.connections.delete(connectionId);
    logger.info('🗑️ Connection removed from map', { connectionId });
  }

  /**
   * Handle incoming WebSocket message
   */
  private async handleMessage(connectionId: string, ws: WebSocket, data: any) {
    try {
      const message = JSON.parse(data.toString());
      logger.info('📨 WebSocket message received', { 
        connectionId, 
        messageType: message.type,
        messageData: message
      });

      if (message.type === 'message') {
        await this.handleChatMessage(connectionId, ws, message);
      } else if (message.type === 'init_conversation') {
        logger.info('🎯 Processing init_conversation message', { connectionId, message });
        await this.handleInitConversation(connectionId, ws, message);
      } else if (message.type === 'save_elevenlabs_message') {
        logger.info('💾 Processing save_elevenlabs_message', { connectionId, message });
        await this.saveElevenLabsMessage(connectionId, message.data);
        ws.send(JSON.stringify({ type: 'message_saved', success: true }));
      } else if (message.type === 'elevenlabs_conversation_started') {
        const connectionData = this.connections.get(connectionId);
        const elevenLabsConversationId = message.elevenLabsConversationId || message.conversationId;
        if (connectionData?.conversationId && elevenLabsConversationId) {
          try {
            await prisma.conversation.update({
              where: { id: connectionData.conversationId },
              data: { elevenLabsConversationId: elevenLabsConversationId }
            });
            logger.info('✅ Stored ElevenLabs conversation ID on conversation', {
              connectionId,
              conversationId: connectionData.conversationId,
              elevenLabsConversationId
            });
          } catch (err) {
            logger.error('❌ Failed to store ElevenLabs conversation ID', {
              connectionId,
              conversationId: connectionData?.conversationId,
              error: (err as any).message
            });
          }
        }
      } else if (message.type === 'ping') {
        ws.send(JSON.stringify({ type: 'pong' }));
      } else {
        logger.warn('⚠️ Unknown message type', { connectionId, messageType: message.type });
      }

    } catch (error) {
      logger.error('❌ Error handling WebSocket message', { 
        connectionId, 
        error: error.message,
        data: data.toString()
      });
      
      ws.send(JSON.stringify({
        type: 'error',
        message: 'Invalid message format'
      }));
    }
  }

  /**
   * Handle conversation initialization
   */
  private async handleInitConversation(connectionId: string, ws: WebSocket, message: any) {
    try {
      const { botId: requestedBotId, botName, userId, conversationId: requestedConversationId } = message;

      if (!botName || !userId) {
        ws.send(JSON.stringify({
          type: 'error',
          message: 'Bot name and user ID are required'
        }));
        return;
      }

      logger.info('🎯 Initializing conversation with ElevenLabs', {
        connectionId,
        requestedBotId,
        botName,
        userId
      });

      // Always resolve the role from the database — never trust the role reported by the client.
      const user = await prisma.user.findUnique({
        where: { id: userId },
        select: { role: true }
      });

      if (!user) {
        ws.send(JSON.stringify({
          type: 'error',
          message: 'User not found'
        }));
        return;
      }
      const finalUserRole = user.role;

      // Determine target bot and conversation to use
      let botIdToUse: string | null = null;
      let agentIdToUse: string | null = null;
      let conversationId: string | null = null;
      let botNameResolved: string = botName;

      // Prefer resuming a specific conversation if provided
      if ((finalUserRole === 'STUDENT' || finalUserRole === 'ADMIN') && requestedConversationId) {
        const convo = await prisma.conversation.findUnique({
          where: { id: requestedConversationId },
          select: {
            id: true,
            endedAt: true,
            studentId: true,
            botId: true,
            bot: { select: { id: true, name: true, agentId: true } }
          }
        });

        if (convo) {
          // Students must own the conversation; admins are allowed
          if (finalUserRole === 'STUDENT' && convo.studentId !== userId) {
            logger.warn('❌ Student attempted to resume a conversation they do not own', {
              connectionId,
              requestedConversationId,
              userId,
              convoOwner: convo.studentId
            });
          } else if (requestedBotId && convo.botId !== requestedBotId) {
            // The bot shown on the client no longer matches the conversation being resumed
            // (e.g. stale UI state). Don't blindly resume the DB-stored bot; fall back to the
            // name-based branch below, which re-validates access from scratch.
            logger.warn('⚠️ Requested botId does not match conversation botId, falling back to name resolution', {
              connectionId,
              requestedConversationId,
              requestedBotId,
              convoBotId: convo.botId,
              userId
            });
          } else if (
            finalUserRole === 'STUDENT' &&
            !(await prisma.studentBotAccess.findFirst({
              where: { studentId: userId, botId: convo.botId, bot: { isActive: true } }
            }))
          ) {
            // The student's access to this bot may have been revoked since the conversation
            // was created. Resuming must re-check access on every request, not just at creation.
            logger.warn('❌ Student no longer has access to the bot of this conversation', {
              connectionId,
              requestedConversationId,
              userId,
              botId: convo.botId
            });
            ws.send(JSON.stringify({
              type: 'error',
              message: 'Bot not found or access denied'
            }));
            return;
          } else {
            conversationId = convo.id;
            botIdToUse = convo.botId;
            agentIdToUse = convo.bot.agentId;
            botNameResolved = convo.bot.name;

            // Block immediately if quota is exhausted
            try {
              const accessCheck = await QuotaService.canUserUseBot(userId);
              if (!accessCheck.canUse) {
                logger.info('🚫 Blocking conversation resume due to quota exhausted', {
                  connectionId,
                  userId,
                  botId: botIdToUse,
                  remainingSeconds: accessCheck.remainingSeconds,
                  reason: accessCheck.reason
                });
                ws.send(JSON.stringify({
                  type: 'quota_exhausted',
                  remainingSeconds: accessCheck.remainingSeconds,
                  reason: accessCheck.reason
                }));
                ws.send(JSON.stringify({
                  type: 'time_expired',
                  message: 'Your time with this bot has expired. Please contact a teacher or admin to reset your timer.'
                }));
                return;
              }
            } catch (lockErr) {
              logger.error('❌ Error checking usage lock on resume', {
                connectionId,
                userId,
                botId: botIdToUse,
                error: (lockErr as any).message
              });
            }

            // Reopen ended conversation
            if (convo.endedAt) {
              await prisma.conversation.update({ where: { id: convo.id }, data: { endedAt: null } });
              logger.info('🔄 Reopened ended conversation for resumption', {
                conversationId,
                userId,
                userRole: finalUserRole,
                previousEndedAt: convo.endedAt
              });
            }

            // Create a new session for this conversation resumption
            const session = await prisma.conversationSession.create({
              data: {
                conversationId: convo.id,
                startedAt: new Date()
              }
            });
            
            // Store session in connection data (will be set later)
            const tempConnectionData = this.connections.get(connectionId);
            if (tempConnectionData) {
              tempConnectionData.sessionId = session.id;
            }
            
            logger.info('📝 Created new session for resumed conversation', {
              conversationId,
              sessionId: session.id,
              userId,
              userRole: finalUserRole
            });

            logger.info('✅ Resuming requested conversation (direct)', {
              conversationId,
              userId,
              userRole: finalUserRole,
              botId: botIdToUse
            });
          }
        }
      }

      // Fallback: resolve by bot name and reuse/create as needed
      if (!conversationId) {
        // Get bot with access validation using actual user role
        const bot = await BotAccessService.getBotByNameWithAccess(userId, botName, finalUserRole);
        logger.info('🔍 Bot lookup result:', {
          connectionId,
          botName,
          userId,
          botFound: !!bot,
          botId: bot?.id,
          agentId: bot?.agentId
        });
        
        if (!bot) {
          ws.send(JSON.stringify({
            type: 'error',
            message: 'Bot not found or access denied'
          }));
          return;
        }

        botIdToUse = bot.id;
        agentIdToUse = bot.agentId;
        botNameResolved = bot.name;

        // Block starting a new conversation if quota is exhausted
        try {
          const accessCheck = await QuotaService.canUserUseBot(userId);
          if (!accessCheck.canUse) {
            logger.info('🚫 Blocking new conversation due to quota exhausted', {
              connectionId,
              userId,
              botId: botIdToUse,
              remainingSeconds: accessCheck.remainingSeconds,
              reason: accessCheck.reason
            });
            ws.send(JSON.stringify({
              type: 'quota_exhausted',
              remainingSeconds: accessCheck.remainingSeconds,
              reason: accessCheck.reason
            }));
            ws.send(JSON.stringify({
              type: 'time_expired',
              message: 'Your time with this bot has expired. Please contact a teacher or admin to reset your timer.'
            }));
            return;
          }
        } catch (lockErr) {
          logger.error('❌ Error checking usage lock before starting conversation', {
            connectionId,
            userId,
            botId: botIdToUse,
            error: (lockErr as any).message
          });
        }

        if (finalUserRole === 'STUDENT' || finalUserRole === 'ADMIN') {
          // Use existing active conversation if any
          const existingConversations = await prisma.conversation.findMany({
            where: { studentId: userId, botId: bot.id, endedAt: null },
            orderBy: { startedAt: 'desc' },
            take: 1
          });
          if (existingConversations.length > 0) {
            conversationId = existingConversations[0].id;
            logger.info('✅ Using existing active conversation', {
              conversationId,
              userId,
              userRole: finalUserRole,
              botId: bot.id
            });
            
            // Create a new session for this existing conversation
            const session = await prisma.conversationSession.create({
              data: {
                conversationId: conversationId,
                startedAt: new Date()
              }
            });
            
            // Store session in connection data (will be set later)
            const tempConnectionData = this.connections.get(connectionId);
            if (tempConnectionData) {
              tempConnectionData.sessionId = session.id;
            }
            
            logger.info('📝 Created new session for existing conversation', {
              conversationId,
              sessionId: session.id,
              userId,
              userRole: finalUserRole
            });
          }

          // Otherwise create new
          if (!conversationId) {
            const conversation = await ConversationService.createConversation({
              studentId: userId,
              botId: bot.id,
              conversationMode: 'TEXT'
            });
            conversationId = conversation.id;
            logger.info('✅ Created new database conversation', {
              conversationId,
              userId,
              userRole: finalUserRole,
              botId: bot.id
            });
            
            // Create a new session for this new conversation
            const session = await prisma.conversationSession.create({
              data: {
                conversationId: conversationId,
                startedAt: new Date()
              }
            });
            
            // Store session in connection data (will be set later)
            const tempConnectionData = this.connections.get(connectionId);
            if (tempConnectionData) {
              tempConnectionData.sessionId = session.id;
            }
            
            logger.info('📝 Created new session for new conversation', {
              conversationId,
              sessionId: session.id,
              userId,
              userRole: finalUserRole
            });
          }
        } else {
          logger.info('ℹ️ Skipping database conversation for user role', {
            userId,
            userRole: finalUserRole,
            botId: bot.id
          });
        }
      }

      // Store connection data BEFORE sending response
      const connectionData = this.connections.get(connectionId);
      if (connectionData) {
        connectionData.agentId = agentIdToUse!;
        connectionData.botId = botIdToUse!;
        connectionData.userId = userId;
        connectionData.conversationId = conversationId;
        connectionData.userRole = finalUserRole;
        
        logger.info('💾 Stored connection data', {
          connectionId,
          agentId: agentIdToUse,
          botId: botIdToUse,
          userId: userId,
          conversationId: conversationId,
          userRole: finalUserRole
        });
      } else {
        logger.error('❌ Connection data not found for connectionId', { connectionId });
      }

        // Timer setup will be done after sending the response

      // Get conversation history if resuming
      let conversationHistory = [];
      if (conversationId && (finalUserRole === 'STUDENT' || finalUserRole === 'ADMIN')) {
        try {
          const conversation = await ConversationService.getConversationWithMessages(conversationId, userId, finalUserRole as any);
          conversationHistory = conversation.messages.map(msg => ({
            role: msg.senderType === 'USER' ? 'user' : 'assistant',
            content: msg.content
          }));
          
          logger.info('📚 Loaded conversation history for resumption', {
            conversationId,
            userRole: finalUserRole,
            messageCount: conversationHistory.length
          });
        } catch (error) {
          logger.error('❌ Error loading conversation history', {
            conversationId,
            userRole: finalUserRole,
            error: (error as any).message
          });
        }
      }

      // Validate agent ID before trying to get signed URL
      logger.info('🔍 Validating agent ID before getting signed URL', {
        connectionId,
        agentId: agentIdToUse
      });
      
      const isAgentValid = await elevenLabsService.validateAgentId(agentIdToUse!);
      if (!isAgentValid) {
        logger.error('❌ Agent ID validation failed', {
          connectionId,
          agentId: agentIdToUse
        });
        ws.send(JSON.stringify({
          type: 'error',
          message: 'Error: The bot is not configured correctly. Please contact the administrator.'
        }));
        return;
      }
      
      logger.info('✅ Agent ID validated successfully', {
        connectionId,
        agentId: agentIdToUse
      });

      // Get signed URL and conversation token from ElevenLabs
      const signedUrl = await elevenLabsService.getSignedUrl(agentIdToUse!);
      let conversationToken: string | undefined;
      try {
        conversationToken = await elevenLabsService.getConversationToken(agentIdToUse!);
      } catch (tokenErr) {
        logger.warn('⚠️ Failed to get conversation token, proceeding with signedUrl only', {
          connectionId,
          agentId: agentIdToUse,
          error: (tokenErr as any)?.message
        });
      }
      
      logger.info('🔗 ElevenLabs credentials obtained', {
        connectionId,
        botName: botNameResolved,
        agentId: agentIdToUse,
        signedUrlLength: signedUrl.length,
        tokenLength: conversationToken?.length || 0,
        hasHistory: conversationHistory.length > 0
      });

      // Best-effort: read the agent's configured language so the frontend can show
      // session messages (end-of-session, etc.) in the bot's own language.
      let agentLanguage: string | null = null;
      try {
        agentLanguage = await elevenLabsService.getAgentLanguage(agentIdToUse!);
      } catch (langErr) {
        logger.warn('⚠️ Failed to fetch agent language', {
          connectionId,
          agentId: agentIdToUse,
          error: (langErr as any)?.message
        });
      }

      // Send credentials to client
      const responseMessage: any = {
        type: 'elevenlabs_ready',
        signedUrl,
        conversationToken,
        agentId: agentIdToUse,
        agentLanguage,
        botName: botNameResolved,
        conversationId: connectionData?.conversationId,
        conversationHistory: conversationHistory
      };

      // Add timer settings if available
      if (botIdToUse) {
        try {
          // Get bot timer settings
          const bot = await prisma.bot.findUnique({
            where: { id: botIdToUse },
            select: { 
              isTimerEnabled: true, 
              maxUsageSeconds: true 
            }
          });

          if (bot && bot.isTimerEnabled && (bot.maxUsageSeconds ?? 0) > 0) {
            // Get current usage stats for the student
            const usageStats = await prisma.studentBotAccess.findUnique({
              where: {
                studentId_botId: {
                  studentId: userId,
                  botId: botIdToUse
                }
              },
              select: {
                totalUsageSeconds: true,
                isLocked: true
              }
            });

            if (usageStats) {
              const remainingSeconds = Math.max(0, bot.maxUsageSeconds - usageStats.totalUsageSeconds);
              
              responseMessage.timerSettings = {
                isTimerEnabled: bot.isTimerEnabled,
                maxUsageSeconds: bot.maxUsageSeconds,
                totalUsageSeconds: usageStats.totalUsageSeconds,
                remainingSeconds: remainingSeconds,
                isLocked: usageStats.isLocked
              };

              logger.info('⏰ Timer settings added to response', {
                connectionId,
                botId: botIdToUse,
                maxUsageSeconds: bot.maxUsageSeconds,
                totalUsageSeconds: usageStats.totalUsageSeconds,
                remainingSeconds,
                isLocked: usageStats.isLocked
              });
            }
          }
        } catch (error) {
          logger.error('❌ Error getting timer settings', {
            connectionId,
            botId: botIdToUse,
            error: error.message
          });
        }
      }
      
      logger.info('📤 Sending elevenlabs_ready message:', {
        connectionId,
        agentId: agentIdToUse,
        botName: botNameResolved,
        hasSignedUrl: !!signedUrl,
        hasToken: !!conversationToken
      });
      
      ws.send(JSON.stringify(responseMessage));
      
      logger.info('✅ elevenlabs_ready message sent successfully');

      // Send initial quota payload immediately so UI can render without waiting for first tick
      try {
        if (finalUserRole === 'STUDENT' && userId) {
          const timerInfo = await QuotaService.getTimerInfo(userId);
          ws.send(JSON.stringify({
            type: 'quota_update',
            remainingSeconds: timerInfo.remainingSeconds,
            totalQuotaSeconds: timerInfo.totalQuotaSeconds,
            usedSeconds: timerInfo.usedSeconds,
            isTimerEnabled: timerInfo.isTimerEnabled
          }));
        }
      } catch (e) {
        logger.warn('⚠️ Failed to send initial quota_update', { connectionId, error: (e as any)?.message });
      }

      // Start per-second bot time updates if timer is enabled and finite
      try {
        const ts = (responseMessage as any).timerSettings;
        const botTimerEnabled = !!ts?.isTimerEnabled && ((ts?.maxUsageSeconds ?? 0) > 0);
        if (botTimerEnabled && connectionData) {
          // Initialize baseline for usage tracking
          connectionData.initialTotalUsage = ts.totalUsageSeconds || 0;
          connectionData.maxUsageSeconds = ts.maxUsageSeconds || 0;
          connectionData.startTime = Date.now();

          const usageInterval = setInterval(() => {
            try {
              if (ws.readyState !== WebSocket.OPEN) {
                clearInterval(usageInterval);
                return;
              }
              const elapsedSecs = Math.floor(((Date.now() - (connectionData.startTime || Date.now())) / 1000));
              const totalUsageSeconds = (connectionData.initialTotalUsage || 0) + Math.max(0, elapsedSecs);
              const maxSecs = connectionData.maxUsageSeconds || 0;
              // Raw value can go negative past the deadline; used to time the
              // farewell warning and the grace period before the hard stop.
              const rawRemainingSeconds = maxSecs > 0 ? (maxSecs - totalUsageSeconds) : 0;
              const remainingSeconds = Math.max(0, rawRemainingSeconds);

              ws.send(JSON.stringify({
                type: 'timer_update',
                totalUsageSeconds,
                remainingSeconds
              }));

              if (maxSecs > 0 && !connectionData.farewellWarningSent && rawRemainingSeconds <= FAREWELL_WARNING_SECONDS) {
                connectionData.farewellWarningSent = true;
                logger.info('⏰ Sending farewell time_warning (bot timer)', { connectionId, remainingSeconds });
                ws.send(JSON.stringify({
                  type: 'time_warning',
                  reason: 'bot_timer',
                  remainingSeconds
                }));
              }

              if (maxSecs > 0 && rawRemainingSeconds <= -HARD_STOP_GRACE_SECONDS) {
                clearInterval(usageInterval);
                logger.info('⏰ Bot timer hard stop after grace period', { connectionId, rawRemainingSeconds });
                // Notify client and close connection on expiry (after the grace period)
                ws.send(JSON.stringify({
                  type: 'time_expired',
                  message: 'Your time with this bot has expired.'
                }));
                if (connectionData.timerInterval) {
                  clearInterval(connectionData.timerInterval);
                }
                ws.close();
              }
            } catch (tickError) {
              logger.error('❌ Error in bot usage timer tick', { connectionId, error: (tickError as any)?.message });
              clearInterval(usageInterval);
            }
          }, 1000);

          // Store for cleanup
          connectionData.usageInterval = usageInterval;
        }
      } catch (e) {
        logger.warn('⚠️ Failed to start bot timer interval', { connectionId, error: (e as any)?.message });
      }

      // Start timer setup for students AFTER sending response (only if timer enabled and finite)
      if (connectionData && finalUserRole === 'STUDENT' && botIdToUse) {
        const quotaManagementEnabled = await systemConfigService.isQuotaManagementEnabledFresh();
        if (!quotaManagementEnabled) {
          logger.info('⏭️ Skipping quota timer (quota management disabled – user can talk without limit)', { connectionId, botId: botIdToUse });
          return;
        }
        const botSettings = await prisma.bot.findUnique({ where: { id: botIdToUse }, select: { isTimerEnabled: true, maxUsageSeconds: true } });
        const shouldRunTimer = !!botSettings?.isTimerEnabled && (botSettings?.maxUsageSeconds ?? 0) > 0;
        if (!shouldRunTimer) {
          logger.info('⏭️ Skipping timer intervals (timer disabled or infinite)', { connectionId, botId: botIdToUse });
          return;
        }
        logger.info('🎯 Starting timer setup for STUDENT', {
          connectionId,
          userId,
          botId: botIdToUse,
          userRole: finalUserRole
        });
        
        try {
          // Get current quota info for this user
          const quotaInfo = await QuotaService.getRemainingSeconds(userId);
          
          logger.info('✅ Quota info retrieved', {
            connectionId,
            userId,
            botId: botIdToUse,
            remainingSeconds: quotaInfo.remainingSeconds,
            allocatedSeconds: quotaInfo.allocatedSeconds
          });
          
          // Initialize simple tracking for conversation start time
          connectionData.startTime = Date.now();
          
          logger.info('⏱️ Quota-based tracking started for student', {
            connectionId,
            userId,
            botId: botIdToUse,
            startTime: connectionData.startTime,
            remainingSeconds: quotaInfo.remainingSeconds
          });

          // Set up periodic quota updates every 10 seconds
          logger.info('⏰ Starting quota timer interval (10 second updates)', {
            connectionId,
            userId,
            botId: botIdToUse,
            startTime: connectionData.startTime
          });
          
          const timerInterval = setInterval(async () => {
            try {
              // Get current quota info from database
              const currentQuota = await QuotaService.getTimerInfo(userId);
              
              logger.info('⏰ Quota timer tick', {
                connectionId,
                userId,
                botId: botIdToUse,
                remainingSeconds: currentQuota.remainingSeconds,
                isQuotaExhausted: currentQuota.isQuotaExhausted
              });
              
              // Send quota update to client
              ws.send(JSON.stringify({
                type: 'quota_update',
                remainingSeconds: currentQuota.remainingSeconds,
                totalQuotaSeconds: currentQuota.totalQuotaSeconds,
                usedSeconds: currentQuota.usedSeconds,
                isTimerEnabled: currentQuota.isTimerEnabled
              }));

              // One-shot farewell warning, same threshold/flag as the bot timer
              if (!connectionData.farewellWarningSent && currentQuota.remainingSeconds > 0 && currentQuota.remainingSeconds <= FAREWELL_WARNING_SECONDS) {
                connectionData.farewellWarningSent = true;
                ws.send(JSON.stringify({
                  type: 'time_warning',
                  reason: 'quota_timer',
                  remainingSeconds: currentQuota.remainingSeconds
                }));
              }

              // Check if quota has been exhausted
              if (currentQuota.isQuotaExhausted && !connectionData.quotaHardStopTimeout) {
                logger.info('⏰ Quota expired for student, scheduling hard stop after grace period', {
                  connectionId,
                  userId,
                  botId: botIdToUse,
                  remainingSeconds: currentQuota.remainingSeconds,
                  graceSeconds: HARD_STOP_GRACE_SECONDS
                });

                // Stop polling; the actual close happens after the grace period below
                clearInterval(timerInterval);
                if (connectionData.usageInterval) {
                  clearInterval(connectionData.usageInterval);
                }

                connectionData.quotaHardStopTimeout = setTimeout(() => {
                  if (ws.readyState === WebSocket.OPEN) {
                    // Notify client and stop conversation
                    ws.send(JSON.stringify({
                      type: 'quota_exhausted',
                      message: 'Your monthly quota has been exhausted. Please contact a teacher or admin for more time.'
                    }));
                    ws.close();
                  }
                }, HARD_STOP_GRACE_SECONDS * 1000);
              }

            } catch (timerError) {
              logger.error('❌ Error in timer update', {
                connectionId,
                userId,
                botId: botIdToUse,
                error: timerError.message
              });
            }
          }, 1000); // Update every 1 second

          // Store the interval ID for cleanup
          connectionData.timerInterval = timerInterval;

          // Note: Usage recording is done when conversation ends, not periodically

        } catch (usageError) {
          logger.error('❌ Error starting usage tracking', {
            connectionId,
            userId,
            botId: botIdToUse,
            error: usageError.message
          });
        }
      }

    } catch (error) {
      logger.error('❌ Error initializing conversation', {
        connectionId,
        error: error.message,
        stack: error.stack
      });
      
      // Send a more user-friendly error message
      let userMessage = 'Error initializing conversation. Please try again.';
      
      if (error.message.includes('no existe')) {
        userMessage = 'Error: The bot is not configured correctly. Please contact the administrator.';
      } else if (error.message.includes('API key')) {
        userMessage = 'Error: Incorrect configuration. Please contact the administrator.';
      } else if (error.message.includes('permisos')) {
        userMessage = 'Error: Permission problem. Please contact the administrator.';
      } else if (error.message.includes('servidor')) {
        userMessage = 'Error: Temporary server problem. Please try again in a few minutes.';
      }
      
      ws.send(JSON.stringify({
        type: 'error',
        message: userMessage
      }));
    }
  }

  /**
   * Handle chat message
   */
  private async handleChatMessage(connectionId: string, ws: WebSocket, message: any) {
    try {
      const { text } = message;
      
      if (!text) {
        ws.send(JSON.stringify({
          type: 'error',
          message: 'Message text is required'
        }));
        return;
      }

      // Get connection data
      const connectionData = this.connections.get(connectionId);
      const agentId = connectionData?.agentId;
      const conversationId = connectionData?.conversationId;

      logger.info('💬 Processing chat message', {
        connectionId,
        agentId,
        conversationId,
        messageLength: text.length,
        hasConnectionData: !!connectionData
      });

      if (!agentId || !conversationId) {
        ws.send(JSON.stringify({
          type: 'error',
          message: 'No agent ID or conversation ID available. Please initialize conversation first.'
        }));
        return;
      }

      // Save user message to database (for students and admins)
      if ((connectionData.userRole === 'STUDENT' || connectionData.userRole === 'ADMIN') && conversationId) {
        try {
          await ConversationService.saveMessage({
            conversationId,
            senderType: 'USER',
            content: text,
            messageType: 'TEXT'
          });

          logger.info('✅ User message saved to database', {
            connectionId,
            conversationId,
            messageLength: text.length
          });

          // Note: Usage time is tracked at conversation level and recorded when conversation ends
          // Individual message tracking is not needed in the quota system
        } catch (error) {
          logger.error('❌ Error saving user message to database', {
            connectionId,
            conversationId,
            error: error.message
          });
          // Don't fail the entire request, just log the error
        }
      } else {
        logger.info('ℹ️ Skipping message save for non-student or no conversation', {
          connectionId,
          userRole: connectionData.userRole,
          hasConversationId: !!conversationId
        });
      }

      // Show typing indicator
      ws.send(JSON.stringify({ type: 'typing', isTyping: true }));

      // Do NOT send or save generic bot acknowledgments.
      // The frontend handles ElevenLabs communication and will forward real bot messages via save_elevenlabs_message.
      
      ws.send(JSON.stringify({ type: 'typing', isTyping: false }));

    } catch (error) {
      logger.error('❌ Error handling chat message', {
        connectionId,
        error: error.message,
        stack: error.stack
      });
      
      ws.send(JSON.stringify({
        type: 'error',
        message: 'Failed to process message'
      }));
    }
  }

  /**
   * Broadcast message to all connected clients
   */
  broadcast(message: any) {
    this.connections.forEach((connectionData, connectionId) => {
      if (connectionData.ws.readyState === WebSocket.OPEN) {
        connectionData.ws.send(JSON.stringify(message));
      }
    });
  }

  /**
   * Send message to specific connection
   */
  sendToConnection(connectionId: string, message: any) {
    const connectionData = this.connections.get(connectionId);
    if (connectionData?.ws && connectionData.ws.readyState === WebSocket.OPEN) {
      connectionData.ws.send(JSON.stringify(message));
    }
  }

  /**
   * Save a message from ElevenLabs (called by frontend)
   */
  async saveElevenLabsMessage(connectionId: string, messageData: any) {
    const connectionData = this.connections.get(connectionId);
    
    if (!connectionData?.conversationId) {
      logger.warn('❌ No conversation ID available for saving message', { connectionId });
      return;
    }

    try {
      const { content, senderType, messageType, audioUrl, audioDuration } = messageData;
      
      await ConversationService.saveMessage({
        conversationId: connectionData.conversationId,
        senderType: senderType || 'BOT',
        content,
        messageType: messageType || 'TEXT',
        audioUrl,
        audioDuration
      });

      logger.info('✅ ElevenLabs message saved to database', {
        connectionId,
        conversationId: connectionData.conversationId,
        senderType: senderType || 'BOT',
        contentLength: content.length,
        hasAudio: !!audioUrl
      });

    } catch (error) {
      logger.error('❌ Error saving ElevenLabs message to database', {
        connectionId,
        conversationId: connectionData.conversationId,
        error: error.message
      });
    }
  }

  /**
   * Get connection count
   */
  getConnectionCount(): number {
    return this.connections.size;
  }

  /**
   * Close all connections
   */
  closeAll() {
    this.connections.forEach((connectionData, connectionId) => {
      logger.info('🔌 Closing WebSocket connection', { connectionId });
      this.handleConnectionClose(connectionId);
    });
  }
}

export const websocketService = WebSocketService.getInstance(); 