import { createServer } from 'http'
import { Server, Socket } from 'socket.io'

// ──────────────────────────────────────────────
// In-memory stores
// ──────────────────────────────────────────────

/** Map socket.id → userId */
const socketToUser = new Map<string, string>()

/** Map userId → Set of socket.ids (a user may have multiple tabs/devices) */
const userToSockets = new Map<string, Set<string>>()

/** Set of currently online userIds (derived from userToSockets keys) */
function getOnlineUserIds(): string[] {
  return Array.from(userToSockets.keys())
}

// ──────────────────────────────────────────────
// Types
// ──────────────────────────────────────────────

type MessageType = 'text' | 'image' | 'file' | 'system'

interface MessagePayload {
  senderId: string
  receiverId: string
  content: string
  messageType: MessageType
}

interface TypingPayload {
  senderId: string
  receiverId: string
}

interface ReadPayload {
  senderId: string
  receiverId: string
  messageIds: string[]
}

interface StoredMessage {
  id: string
  senderId: string
  receiverId: string
  content: string
  messageType: MessageType
  timestamp: string
  delivered: boolean
  read: boolean
}

// Simple in-memory message store (recent messages per conversation)
const messageStore = new Map<string, StoredMessage[]>()

function getConversationKey(userA: string, userB: string): string {
  return [userA, userB].sort().join('::')
}

function generateId(): string {
  return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
}

// ──────────────────────────────────────────────
// Socket.IO Server
// ──────────────────────────────────────────────

const httpServer = createServer()
const io = new Server(httpServer, {
  // DO NOT change the path — Caddy uses it to route to this service
  path: '/',
  cors: {
    origin: '*',
    methods: ['GET', 'POST'],
  },
  pingTimeout: 60000,
  pingInterval: 25000,
})

// ──────────────────────────────────────────────
// Helper: register a socket↔user mapping
// ──────────────────────────────────────────────

function registerSocket(socketId: string, userId: string): void {
  socketToUser.set(socketId, userId)
  if (!userToSockets.has(userId)) {
    userToSockets.set(userId, new Set())
  }
  userToSockets.get(userId)!.add(socketId)
}

function unregisterSocket(socketId: string): void {
  const userId = socketToUser.get(socketId)
  if (userId) {
    socketToUser.delete(socketId)
    const sockets = userToSockets.get(userId)
    if (sockets) {
      sockets.delete(socketId)
      if (sockets.size === 0) {
        userToSockets.delete(userId)
      }
    }
  }
}

function isUserOnline(userId: string): boolean {
  return userToSockets.has(userId) && userToSockets.get(userId)!.size > 0
}

// ──────────────────────────────────────────────
// Connection handler
// ──────────────────────────────────────────────

io.on('connection', (socket: Socket) => {
  const connectTime = new Date().toISOString()
  console.log(`[${connectTime}] Socket connected: ${socket.id}`)

  // ── join ──────────────────────────────────
  socket.on('join', (data: { userId: string }) => {
    try {
      const { userId } = data
      if (!userId) {
        socket.emit('error', { message: 'userId is required' })
        return
      }

      registerSocket(socket.id, userId)

      // Join the user's personal room so we can emit directly to them
      socket.join(userId)

      console.log(`[${new Date().toISOString()}] User ${userId} joined (socket ${socket.id})`)

      // Confirm join to the client
      socket.emit('joined', {
        userId,
        socketId: socket.id,
        onlineUsers: getOnlineUserIds(),
      })

      // Broadcast updated online presence to everyone
      io.emit('presence', { onlineUsers: getOnlineUserIds() })
    } catch (err) {
      console.error(`[join] Error for socket ${socket.id}:`, err)
      socket.emit('error', { message: 'Failed to join' })
    }
  })

  // ── message ───────────────────────────────
  socket.on('message', (data: MessagePayload) => {
    try {
      const { senderId, receiverId, content, messageType } = data

      if (!senderId || !receiverId || !content) {
        socket.emit('error', { message: 'senderId, receiverId, and content are required' })
        return
      }

      const messageId = generateId()
      const timestamp = new Date().toISOString()

      const storedMessage: StoredMessage = {
        id: messageId,
        senderId,
        receiverId,
        content,
        messageType: messageType || 'text',
        timestamp,
        delivered: isUserOnline(receiverId),
        read: false,
      }

      // Store in conversation history (keep last 200 per conversation)
      const convKey = getConversationKey(senderId, receiverId)
      const history = messageStore.get(convKey) || []
      history.push(storedMessage)
      if (history.length > 200) history.splice(0, history.length - 200)
      messageStore.set(convKey, history)

      // Emit to receiver's personal room
      io.to(receiverId).emit('message', {
        id: messageId,
        senderId,
        receiverId,
        content,
        messageType: storedMessage.messageType,
        timestamp,
      })

      // Delivery confirmation back to sender
      socket.emit('message-delivered', {
        messageId,
        senderId,
        receiverId,
        timestamp,
        delivered: storedMessage.delivered,
      })

      console.log(
        `[${timestamp}] Message ${messageId}: ${senderId} → ${receiverId} (${storedMessage.messageType})`
      )
    } catch (err) {
      console.error(`[message] Error for socket ${socket.id}:`, err)
      socket.emit('error', { message: 'Failed to process message' })
    }
  })

  // ── typing ────────────────────────────────
  socket.on('typing', (data: TypingPayload) => {
    try {
      const { senderId, receiverId } = data
      if (!senderId || !receiverId) return

      io.to(receiverId).emit('typing', { senderId, receiverId })
    } catch (err) {
      console.error(`[typing] Error for socket ${socket.id}:`, err)
    }
  })

  // ── stop-typing ───────────────────────────
  socket.on('stop-typing', (data: TypingPayload) => {
    try {
      const { senderId, receiverId } = data
      if (!senderId || !receiverId) return

      io.to(receiverId).emit('stop-typing', { senderId, receiverId })
    } catch (err) {
      console.error(`[stop-typing] Error for socket ${socket.id}:`, err)
    }
  })

  // ── read ──────────────────────────────────
  socket.on('read', (data: ReadPayload) => {
    try {
      const { senderId, receiverId, messageIds } = data
      if (!senderId || !receiverId || !Array.isArray(messageIds) || messageIds.length === 0) {
        socket.emit('error', { message: 'senderId, receiverId, and messageIds[] are required' })
        return
      }

      const timestamp = new Date().toISOString()

      // Update stored messages
      const convKey = getConversationKey(senderId, receiverId)
      const history = messageStore.get(convKey)
      if (history) {
        for (const msg of history) {
          if (
            messageIds.includes(msg.id) &&
            msg.senderId === senderId &&
            msg.receiverId === receiverId
          ) {
            msg.read = true
          }
        }
      }

      // Notify the original sender that their messages were read
      io.to(senderId).emit('read', {
        readerId: receiverId,
        messageIds,
        timestamp,
      })

      console.log(
        `[${timestamp}] Read receipt: ${receiverId} read ${messageIds.length} message(s) from ${senderId}`
      )
    } catch (err) {
      console.error(`[read] Error for socket ${socket.id}:`, err)
      socket.emit('error', { message: 'Failed to process read receipt' })
    }
  })

  // ── disconnect ────────────────────────────
  socket.on('disconnect', (reason) => {
    const userId = socketToUser.get(socket.id)
    unregisterSocket(socket.id)

    const disconnectTime = new Date().toISOString()
    if (userId) {
      console.log(
        `[${disconnectTime}] User ${userId} disconnected (socket ${socket.id}, reason: ${reason}). Still online: ${isUserOnline(userId)}`
      )
      // If user fully offline, broadcast updated presence
      if (!isUserOnline(userId)) {
        io.emit('presence', { onlineUsers: getOnlineUserIds() })
      }
    } else {
      console.log(`[${disconnectTime}] Socket ${socket.id} disconnected (reason: ${reason})`)
    }
  })

  // ── error ─────────────────────────────────
  socket.on('error', (error) => {
    console.error(`[error] Socket ${socket.id} error:`, error)
  })
})

// ──────────────────────────────────────────────
// Start server
// ──────────────────────────────────────────────

const PORT = 3003

httpServer.listen(PORT, () => {
  console.log(`🚀 Needyfy Chat Service running on port ${PORT}`)
  console.log(`   Socket.IO path: /`)
  console.log(`   CORS: *`)
  console.log(`   Ready for connections...`)
})

// ──────────────────────────────────────────────
// Graceful shutdown
// ──────────────────────────────────────────────

function gracefulShutdown(signal: string) {
  console.log(`\nReceived ${signal}, shutting down gracefully...`)

  // Notify all connected clients
  io.emit('server-shutdown', { message: 'Server is shutting down' })

  io.disconnectSockets(true)

  httpServer.close(() => {
    console.log('HTTP server closed')
    process.exit(0)
  })

  // Force exit after 10 seconds if graceful shutdown hangs
  setTimeout(() => {
    console.error('Forcing shutdown after timeout')
    process.exit(1)
  }, 10000)
}

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
process.on('SIGINT', () => gracefulShutdown('SIGINT'))

process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err)
})

process.on('unhandledRejection', (reason) => {
  console.error('Unhandled Rejection:', reason)
})
