const app = require('./app');
const { socketAuth } = require('./middleware/auth');
const { Server } = require('socket.io');
const { createServer } = require('http');
const Conversation = require('./models/conversation.model');
const Message = require('./models/message.model');
const Account = require('./models/account.model');
const NodeCache = require('node-cache');
const mongoose = require('mongoose');

const httpServer = createServer(app);
const io = new Server(httpServer, {
  cors: {
    origin: '*',
    methods: ['GET', 'POST'],
  },
  // Add ping timeout and interval for better connection handling
  pingTimeout: 60000,
  pingInterval: 25000,
});

// Conversation cache
const convCache = new NodeCache({ stdTTL: 600 }); // 10-minute cache
const activeUsers = new Map(); // Track active users and their socket IDs

// Socket authentication middleware
io.use(socketAuth);

io.on('connection', (socket) => {
  console.log(
    `${socket.user.firstName} (${socket.user.role}) connected - Socket ID: ${socket.id}`,
  );

  // Store user's socket ID for direct messaging
  if (!activeUsers.has(socket.user.id)) {
    activeUsers.set(socket.user.id, new Set());
  }
  activeUsers.get(socket.user.id).add(socket.id);

  // Join user's personal room for notifications
  socket.join(`user_${socket.user.id}`);

  // Join admin room if user is admin or superadmin
  if (socket.user.role === 'ADMIN' || socket.user.role === 'SUPER_ADMIN') {
    socket.join('admin_room');

    // Notify other admins that this admin is online
    socket.to('admin_room').emit('adminOnline', {
      adminId: socket.user.id,
      adminName: `${socket.user.firstName} ${socket.user.lastName}`,
    });
  }

  // Handle joining a specific conversation room
  socket.on('joinConversation', async (conversationId) => {
    try {
      if (!mongoose.Types.ObjectId.isValid(conversationId)) {
        return socket.emit('error', {
          message: 'Invalid conversation ID format',
        });
      }

      const conversation = await Conversation.findById(conversationId)
        .populate('user', 'id firstName lastName email avatar')
        .populate('admin', 'id firstName lastName email avatar');

      if (!conversation) {
        return socket.emit('error', { message: 'Conversation not found' });
      }

      // Check if user has permission to join this conversation
      const canJoin =
        conversation.user._id.toString() === socket.user.id || // User is the conversation owner
        socket.user.role === 'ADMIN' || // User is admin
        socket.user.role === 'SUPER_ADMIN' || // User is superadmin
        (conversation.admin &&
          conversation.admin._id.toString() === socket.user.id); // User is assigned admin

      if (!canJoin) {
        return socket.emit('error', {
          message: 'Not authorized to join this conversation',
        });
      }

      // Leave any previous conversation rooms (except personal and admin rooms)
      const rooms = Array.from(socket.rooms);
      rooms.forEach((room) => {
        if (room.startsWith('conversation_')) {
          socket.leave(room);
        }
      });

      // Join the conversation room
      const roomName = `conversation_${conversationId}`;
      socket.join(roomName);

      // If admin is joining and not assigned, assign them
      if (
        (socket.user.role === 'ADMIN' || socket.user.role === 'SUPER_ADMIN') &&
        !conversation.admin
      ) {
        conversation.admin = socket.user.id;
        conversation.status = 'active';
        await conversation.save();
      }

      // Get recent messages for this conversation
      const messages = await Message.find({ conversation: conversationId })
        .populate('sender', 'id firstName lastName email avatar role')
        .sort({ createdAt: 1 })
        .limit(50); // Get last 50 messages

      socket.emit('conversationJoined', {
        conversation,
        messages,
      });

      // Notify others in the conversation that someone joined
      socket.to(roomName).emit('userJoinedConversation', {
        userId: socket.user.id,
        userName: `${socket.user.firstName} ${socket.user.lastName}`,
        userRole: socket.user.role,
      });

      console.log(
        `${socket.user.firstName} joined conversation ${conversationId}`,
      );
    } catch (error) {
      console.error('Error joining conversation:', error);
      socket.emit('error', { message: 'Failed to join conversation' });
    }
  });

  // Handle leaving a conversation room
  socket.on('leaveConversation', async (conversationId) => {
    try {
      const roomName = `conversation_${conversationId}`;
      socket.leave(roomName);

      socket.to(roomName).emit('userLeftConversation', {
        userId: socket.user.id,
        userName: `${socket.user.firstName} ${socket.user.lastName}`,
      });

      console.log(
        `${socket.user.firstName} left conversation ${conversationId}`,
      );
    } catch (error) {
      console.error('Error leaving conversation:', error);
    }
  });

  // Handle sending messages
  socket.on('sendMessage', async (data) => {
    try {
      let {
        conversationId,
        content,
        messageType = 'text',
        attachments = [],
      } = data;

      if (!content && attachments.length === 0) {
        return socket.emit('error', {
          message: 'Message content or attachments are required',
        });
      }

      let conversation;

      // If user is admin and no conversationId provided, return error
      if (
        (socket.user.role === 'ADMIN' || socket.user.role === 'SUPER_ADMIN') &&
        !conversationId
      ) {
        return socket.emit('error', {
          message: 'Conversation ID is required for admin messages',
        });
      }

      // If conversationId is provided, find the conversation
      if (conversationId) {
        if (!mongoose.Types.ObjectId.isValid(conversationId)) {
          return socket.emit('error', {
            message: 'Invalid conversation ID format',
          });
        }

        conversation = await Conversation.findById(conversationId);

        if (!conversation) {
          return socket.emit('error', { message: 'Conversation not found' });
        }
      }
      // If no conversationId and user is regular user, create a new conversation
      else if (socket.user.role === 'USER') {
        // Check if user already has an open conversation
        const existingConversation = await Conversation.findOne({
          user: socket.user.id,
          status: { $ne: 'closed' },
        });

        if (existingConversation) {
          conversation = existingConversation;
        } else {
          // Create new conversation
          const [newConversation] = await Conversation.create([
            {
              user: socket.user.id,
              subject: content ? content.slice(0, 50) : 'Support Request',
              status: 'pending',
              lastMessageAt: new Date(),
            },
          ]);
          conversation = newConversation;

          // Join the new conversation room
          socket.join(`conversation_${conversation._id}`);
        }
      } else {
        return socket.emit('error', { message: 'Conversation not found' });
      }

      // Check permissions
      const canSend =
        conversation.user.toString() === socket.user.id || // User is the conversation owner
        socket.user.role === 'ADMIN' || // User is admin
        socket.user.role === 'SUPER_ADMIN' || // User is superadmin
        (conversation.admin &&
          conversation.admin.toString() === socket.user.id); // User is assigned admin

      if (!canSend) {
        return socket.emit('error', {
          message: 'Not authorized to send messages in this conversation',
        });
      }

      // Create the message
      const [newMessage] = await Message.create([
        {
          conversation: conversation._id,
          sender: socket.user.id,
          content: content || '',
          type: messageType,
          attachments: attachments,
          isRead: false,
        },
      ]);

      // Update conversation's last message time
      conversation.lastMessageAt = new Date();
      await conversation.save();

      // Get the message with sender info
      const messageWithSender = await Message.findById(newMessage._id).populate(
        'sender',
        'id firstName lastName email avatar role',
      );

      const roomName = `conversation_${conversation._id}`;

      // Broadcast message to all users in the conversation room
      io.to(roomName).emit('newMessage', messageWithSender);

      // If message is from user, notify all admins
      if (socket.user.role === 'USER') {
        io.to('admin_room').emit('newUserMessage', {
          conversationId: conversation._id,
          message: messageWithSender,
          conversation: {
            id: conversation._id,
            subject: conversation.subject,
            user: conversation.user,
          },
        });
      }

      // If message is from admin, notify the specific user
      if (socket.user.role === 'ADMIN' || socket.user.role === 'SUPER_ADMIN') {
        io.to(`user_${conversation.user}`).emit('newAdminMessage', {
          conversationId: conversation._id,
          message: messageWithSender,
        });
      }

      console.log(
        `Message sent in conversation ${conversation._id} by ${socket.user.firstName}`,
      );
    } catch (error) {
      console.error('Error sending message:', error);
      socket.emit('error', {
        message: 'Failed to send message: ' + error.message,
      });
    }
  });

  // Handle marking messages as read
  socket.on('markAsRead', async (data) => {
    try {
      const { conversationId, messageIds } = data;

      if (!conversationId) {
        return socket.emit('error', { message: 'Conversation ID is required' });
      }

      if (!mongoose.Types.ObjectId.isValid(conversationId)) {
        return socket.emit('error', {
          message: 'Invalid conversation ID format',
        });
      }

      // Build query
      const query = {
        conversation: conversationId,
        isRead: false,
        sender: { $ne: socket.user.id }, // Don't mark own messages as read
      };

      if (messageIds && messageIds.length > 0) {
        // Validate message IDs
        const validIds = messageIds.filter((id) =>
          mongoose.Types.ObjectId.isValid(id),
        );
        if (validIds.length > 0) {
          query._id = { $in: validIds };
        }
      }

      // Update messages as read
      const result = await Message.updateMany(query, {
        isRead: true,
        readAt: new Date(),
      });

      // Notify others in the conversation
      socket.to(`conversation_${conversationId}`).emit('messagesMarkedAsRead', {
        conversationId,
        readBy: socket.user.id,
        messageIds,
        count: result.modifiedCount,
      });

      console.log(
        `${result.modifiedCount} messages marked as read in conversation ${conversationId}`,
      );
    } catch (error) {
      console.error('Error marking messages as read:', error);
      socket.emit('error', { message: 'Failed to mark messages as read' });
    }
  });

  // Handle typing indicators
  socket.on('typing', (data) => {
    const { conversationId, isTyping } = data;

    if (!conversationId) return;

    socket.to(`conversation_${conversationId}`).emit('userTyping', {
      userId: socket.user.id,
      userName: `${socket.user.firstName} ${socket.user.lastName}`,
      isTyping,
    });
  });

  // Handle getting online users for a conversation
  socket.on('getOnlineUsers', async (conversationId) => {
    try {
      if (!mongoose.Types.ObjectId.isValid(conversationId)) {
        return socket.emit('error', {
          message: 'Invalid conversation ID format',
        });
      }

      const socketsInRoom = await io
        .in(`conversation_${conversationId}`)
        .fetchSockets();

      const onlineUsers = socketsInRoom.map((s) => ({
        userId: s.user.id,
        userName: `${s.user.firstName} ${s.user.lastName}`,
        userRole: s.user.role,
        socketId: s.id,
      }));

      socket.emit('onlineUsers', { conversationId, users: onlineUsers });
    } catch (error) {
      console.error('Error getting online users:', error);
      socket.emit('error', { message: 'Failed to get online users' });
    }
  });

  // Handle getting user's conversations list
  socket.on('getMyConversations', async () => {
    try {
      let conversations;

      if (socket.user.role === 'ADMIN' || socket.user.role === 'SUPER_ADMIN') {
        // Admins see all conversations
        conversations = await Conversation.find({ status: { $ne: 'closed' } })
          .populate('user', 'id firstName lastName email avatar')
          .populate('admin', 'id firstName lastName email avatar')
          .sort({ lastMessageAt: -1, createdAt: -1 })
          .limit(50);
      } else {
        // Users see only their own conversations
        conversations = await Conversation.find({
          user: socket.user.id,
          status: { $ne: 'closed' },
        })
          .populate('user', 'id firstName lastName email avatar')
          .populate('admin', 'id firstName lastName email avatar')
          .sort({ lastMessageAt: -1, createdAt: -1 });
      }

      // Get unread counts for each conversation
      const conversationsWithUnread = await Promise.all(
        conversations.map(async (conv) => {
          const unreadCount = await Message.countDocuments({
            conversation: conv._id,
            isRead: false,
            sender: { $ne: socket.user.id },
          });

          return {
            ...conv.toObject(),
            unreadCount,
          };
        }),
      );

      socket.emit('myConversations', conversationsWithUnread);
    } catch (error) {
      console.error('Error fetching conversations:', error);
      socket.emit('error', { message: 'Failed to fetch conversations' });
    }
  });

  // Handle disconnection
  socket.on('disconnect', () => {
    console.log(
      `${socket.user.firstName} (${socket.user.role}) disconnected - Socket ID: ${socket.id}`,
    );

    // Remove from active users
    if (activeUsers.has(socket.user.id)) {
      const userSockets = activeUsers.get(socket.user.id);
      userSockets.delete(socket.id);
      if (userSockets.size === 0) {
        activeUsers.delete(socket.user.id);
      }
    }

    // Notify admin room if admin disconnected
    if (socket.user.role === 'ADMIN' || socket.user.role === 'SUPER_ADMIN') {
      socket.to('admin_room').emit('adminOffline', {
        adminId: socket.user.id,
        adminName: `${socket.user.firstName} ${socket.user.lastName}`,
      });
    }

    // Notify conversations that user left
    const rooms = Array.from(socket.rooms);
    rooms.forEach((room) => {
      if (room.startsWith('conversation_')) {
        socket.to(room).emit('userLeftConversation', {
          userId: socket.user.id,
          userName: `${socket.user.firstName} ${socket.user.lastName}`,
        });
      }
    });
  });

  // Handle connection errors
  socket.on('error', (error) => {
    console.error('Socket error:', error);
  });

  // Send initial connection confirmation
  socket.emit('connected', {
    userId: socket.user.id,
    userName: `${socket.user.firstName} ${socket.user.lastName}`,
    userRole: socket.user.role,
    activeUsersCount: activeUsers.size,
  });
});

// Periodic status broadcast (every 30 seconds)
setInterval(() => {
  const adminSockets = io.sockets.adapter.rooms.get('admin_room');
  const adminCount = adminSockets ? adminSockets.size : 0;

  io.to('admin_room').emit('adminStats', {
    onlineAdmins: adminCount,
    activeUsers: activeUsers.size,
    timestamp: new Date(),
  });
}, 30000);

module.exports = {
  io,
  httpServer,
};
