import type { Server, Socket } from 'socket.io'
import { z } from 'zod'
import { validationError } from '../lib/errors.js'
import { meetingCodeSchema } from '../lib/meetingCode.js'
import type { Identity } from '../lib/tokens.js'
import { logger } from '../lib/logger.js'
import { Participant } from '../models/Participant.js'
import * as meetingService from '../services/meetingService.js'
import {
  addPeer,
  addWaiting,
  peerByParticipant,
  peerBySocket,
  publicPeer,
  removePeer,
  removeWaitingSocket,
  roomPeers,
  takeWaiting,
  updatePeer,
  waitingPeers,
  type LivePeer,
} from './rooms.js'

const joinSchema = z.object({
  meetingCode: z.string().regex(meetingCodeSchema),
  displayName: z.string().trim().min(1).max(80),
  audio: z.boolean(),
  video: z.boolean(),
})

const signalSchema = z.object({
  to: z.string().min(1),
  description: z
    .object({
      type: z.enum(['offer', 'answer']),
      sdp: z.string().min(1),
    })
    .optional(),
  candidate: z
    .object({
      candidate: z.string(),
      sdpMid: z.string().nullable().optional(),
      sdpMLineIndex: z.number().nullable().optional(),
      usernameFragment: z.string().nullable().optional(),
    })
    .optional(),
})

const chatSchema = z.object({ message: z.string().trim().min(1).max(2000) })
const idSchema = z.object({ participantId: z.string().min(1) })
const mediaSchema = z.object({ enabled: z.boolean() })

const chatHits = new Map<string, number[]>()

const allowChat = (socketId: string) => {
  const now = Date.now()
  const recent = (chatHits.get(socketId) ?? []).filter((stamp) => now - stamp < 10_000)
  if (recent.length >= 8) return false
  recent.push(now)
  chatHits.set(socketId, recent)
  return true
}

const sameRoomTarget = (from: LivePeer, code: string, to: string) => {
  const target = roomPeers(code).find((peer) => peer.socketId === to)
  if (!target || target.socketId === from.socketId) return null
  return target
}

export const registerSignaling = (io: Server) => {
  io.on('connection', (socket: Socket) => {
    const identity = socket.data.identity as Identity | undefined
    if (!identity) {
      socket.disconnect(true)
      return
    }

    socket.on('join-room', async (raw: unknown, ack?: (payload: unknown) => void) => {
      const parsed = joinSchema.safeParse(raw)
      if (!parsed.success) {
        ack?.({ error: 'Invalid join payload' })
        return
      }
      try {
        const meeting = await meetingService.getMeetingByCode(parsed.data.meetingCode)
        if (meeting.status === 'ended') {
          ack?.({ error: 'This meeting has ended' })
          return
        }
        const host = meetingService.toMeetingDto(meeting, identity).isHost
        const settings = meetingService.settingsOf(meeting)
        const previous = await Participant.findOne(
          identity.kind === 'user'
            ? { meetingId: meeting._id, userId: identity.sub }
            : { meetingId: meeting._id, guestId: identity.sub },
        )
        if (previous?.status === 'removed') {
          ack?.({ error: 'The host removed you from this meeting' })
          return
        }

        const alreadyInside = previous?.status === 'admitted' || previous?.status === 'left'
        const mustWait = settings.waitingRoom && !host && !alreadyInside
        const participant = await meetingService.recordParticipant({
          meetingId: meeting._id,
          identity,
          displayName: parsed.data.displayName,
          role: host ? 'host' : 'participant',
          status: mustWait ? 'waiting' : 'admitted',
        })
        if (!participant) throw validationError('Could not join')

        if (mustWait) {
          addWaiting(meeting.meetingCode, {
            socketId: socket.id,
            participantId: participant._id.toString(),
            identitySub: identity.sub,
            name: parsed.data.displayName,
          })
          socket.join(`lobby:${meeting.meetingCode}`)
          io.to(`room:${meeting.meetingCode}`).emit('participant-waiting', {
            participantId: participant._id.toString(),
            name: parsed.data.displayName,
          })
          ack?.({ status: 'waiting', participantId: participant._id.toString() })
          return
        }

        const peer: LivePeer = {
          socketId: socket.id,
          participantId: participant._id.toString(),
          identitySub: identity.sub,
          name: parsed.data.displayName,
          role: host ? 'host' : 'participant',
          audio: parsed.data.audio,
          video: parsed.data.video,
          screen: false,
        }
        const others = roomPeers(meeting.meetingCode).map(publicPeer)
        addPeer(meeting.meetingCode, peer)
        socket.join(`room:${meeting.meetingCode}`)
        ack?.({
          status: 'joined',
          self: publicPeer(peer),
          peers: others,
          waiting: host ? waitingPeers(meeting.meetingCode) : [],
          settings,
        })
        socket.to(`room:${meeting.meetingCode}`).emit('user-joined', publicPeer(peer))
      } catch (error) {
        logger.warn('join-room failed', error)
        ack?.({ error: error instanceof Error ? error.message : 'Could not join' })
      }
    })

    socket.on('host-admit', async (raw: unknown) => {
      const located = peerBySocket(socket.id)
      const parsed = idSchema.safeParse(raw)
      if (!located || located.peer.role !== 'host' || !parsed.success) return
      const waiting = takeWaiting(located.code, parsed.data.participantId)
      if (!waiting) return
      await Participant.updateOne(
        { _id: waiting.participantId },
        { status: 'admitted', joinedAt: new Date(), leftAt: null },
      )
      io.to(waiting.socketId).emit('admitted', { meetingCode: located.code })
      socket.emit('waiting-list', waitingPeers(located.code))
    })

    socket.on('host-deny', async (raw: unknown) => {
      const located = peerBySocket(socket.id)
      const parsed = idSchema.safeParse(raw)
      if (!located || located.peer.role !== 'host' || !parsed.success) return
      const waiting = takeWaiting(located.code, parsed.data.participantId)
      if (!waiting) return
      await meetingService.markParticipantLeft(waiting.participantId, 'removed')
      io.to(waiting.socketId).emit('denied')
      socket.emit('waiting-list', waitingPeers(located.code))
    })

    socket.on('host-mute', (raw: unknown) => {
      const located = peerBySocket(socket.id)
      const parsed = idSchema.safeParse(raw)
      if (!located || located.peer.role !== 'host' || !parsed.success) return
      const target = peerByParticipant(located.code, parsed.data.participantId)
      if (!target) return
      io.to(target.socketId).emit('force-mute')
    })

    socket.on('host-remove', async (raw: unknown) => {
      const located = peerBySocket(socket.id)
      const parsed = idSchema.safeParse(raw)
      if (!located || located.peer.role !== 'host' || !parsed.success) return
      const target = peerByParticipant(located.code, parsed.data.participantId)
      if (!target || target.role === 'host') return
      removePeer(located.code, target.socketId)
      await meetingService.markParticipantLeft(target.participantId, 'removed')
      io.to(target.socketId).emit('removed')
      io.to(`room:${located.code}`).emit('user-left', { socketId: target.socketId, participantId: target.participantId })
      io.sockets.sockets.get(target.socketId)?.leave(`room:${located.code}`)
    })

    const relay = (event: 'offer' | 'answer' | 'ice-candidate') => {
      socket.on(event, (raw: unknown) => {
        const located = peerBySocket(socket.id)
        const parsed = signalSchema.safeParse(raw)
        if (!located || !parsed.success) return
        const target = sameRoomTarget(located.peer, located.code, parsed.data.to)
        if (!target) return
        if (event === 'ice-candidate' && !parsed.data.candidate) return
        if (event !== 'ice-candidate' && !parsed.data.description) return
        io.to(target.socketId).emit(event, {
          from: socket.id,
          description: parsed.data.description,
          candidate: parsed.data.candidate,
        })
      })
    }
    relay('offer')
    relay('answer')
    relay('ice-candidate')

    const toggle = (event: 'toggle-audio' | 'toggle-video', key: 'audio' | 'video') => {
      socket.on(event, (raw: unknown) => {
        const located = peerBySocket(socket.id)
        const parsed = mediaSchema.safeParse(raw)
        if (!located || !parsed.success) return
        const peer = updatePeer(located.code, socket.id, { [key]: parsed.data.enabled })
        if (!peer) return
        socket.to(`room:${located.code}`).emit('peer-updated', publicPeer(peer))
      })
    }
    toggle('toggle-audio', 'audio')
    toggle('toggle-video', 'video')

    socket.on('screen-share-start', () => {
      const located = peerBySocket(socket.id)
      if (!located) return
      void meetingService.getMeetingByCode(located.code).then((meeting) => {
        if (!meetingService.settingsOf(meeting).allowScreenShare) {
          socket.emit('screen-share-blocked')
          return
        }
        const peer = updatePeer(located.code, socket.id, { screen: true })
        if (peer) socket.to(`room:${located.code}`).emit('peer-updated', publicPeer(peer))
      })
    })

    socket.on('screen-share-stop', () => {
      const located = peerBySocket(socket.id)
      if (!located) return
      const peer = updatePeer(located.code, socket.id, { screen: false })
      if (peer) socket.to(`room:${located.code}`).emit('peer-updated', publicPeer(peer))
    })

    socket.on('chat-message', async (raw: unknown) => {
      const located = peerBySocket(socket.id)
      const parsed = chatSchema.safeParse(raw)
      if (!located || !parsed.success) return
      if (!allowChat(socket.id)) {
        socket.emit('chat-error', { message: 'Slow down a little' })
        return
      }
      try {
        const meeting = await meetingService.getMeetingByCode(located.code)
        if (!meetingService.settingsOf(meeting).allowChat) {
          socket.emit('chat-error', { message: 'Chat is disabled for this meeting' })
          return
        }
        const saved = await meetingService.saveMessage({
          meetingId: meeting._id,
          senderId: located.peer.identitySub,
          senderName: located.peer.name,
          message: parsed.data.message,
        })
        io.to(`room:${located.code}`).emit('chat-message', saved)
      } catch (error) {
        logger.warn('chat-message failed', error)
        socket.emit('chat-error', { message: 'Could not send message' })
      }
    })

    socket.on('disconnect', async () => {
      chatHits.delete(socket.id)
      const waitingCode = removeWaitingSocket(socket.id)
      if (waitingCode) {
        io.to(`room:${waitingCode}`).emit('waiting-list', waitingPeers(waitingCode))
      }
      const located = peerBySocket(socket.id)
      if (!located) return
      removePeer(located.code, socket.id)
      await meetingService.markParticipantLeft(located.peer.participantId, 'left')
      socket.to(`room:${located.code}`).emit('user-left', {
        socketId: socket.id,
        participantId: located.peer.participantId,
      })
    })
  })
}
