import type { types } from 'mediasoup'
import type { Transport } from 'mediasoup/types'
import type { Server as SocketServer } from 'socket.io'
import type {
  ChadClient,
  SomeSocket,
} from '../types/webrtc.ts'
import { consola } from 'consola'
import prisma from '../prisma/client.ts'
import { socketToClient } from '../utils/socket-to-client.ts'

export default async function (io: SocketServer, router: types.Router) {
  const audioLevelObserver = await router.createAudioLevelObserver({
    maxEntries: 10,
    threshold: -80,
    interval: 800,
  })

  const activeSpeakerObserver = await router.createActiveSpeakerObserver()

  audioLevelObserver.on('volumes', async (volumes: types.AudioLevelObserverVolume[]) => {
    io.emit('speakingPeers', volumes.map(({ producer, volume }) => {
      const { socketId } = producer.appData as { socketId: ChadClient['socketId'] }

      return {
        clientId: socketId,
        volume,
      }
    }))
  })

  audioLevelObserver.on('silence', () => {
    io.emit('speakingPeers', [])
    io.emit('activeSpeaker', undefined)
  })

  activeSpeakerObserver.on('dominantspeaker', ({ producer }) => {
    const { socketId } = producer.appData as { socketId: ChadClient['socketId'] }

    io.emit('activeSpeaker', socketId)
  })

  io.on('connection', async (socket) => {
    consola.info('[WebRtc]', 'Client connected', socket.id)

    socket.data.joined = false

    socket.data.inputMuted = false
    socket.data.outputMuted = false

    socket.data.transports = new Map()
    socket.data.producers = new Map()
    socket.data.consumers = new Map()

    const { id, username, displayName } = await prisma.user.findUnique({
      where: {
        id: socket.handshake.auth.userId,
      },
      select: {
        id: true,
        username: true,
        displayName: true,
      },
    })

    socket.data.userId = id
    socket.data.username = username
    socket.data.displayName = displayName

    socket.emit('authenticated')

    socket.on('join', async ({ rtpCapabilities }, cb) => {
      if (socket.data.joined) {
        consola.error('[WebRtc]', 'Already joined')
        cb({ error: 'Already joined' })
      }

      socket.data.joined = true
      socket.data.rtpCapabilities = rtpCapabilities

      const joinedSockets = await getJoinedSockets()

      cb(joinedSockets.map(socketToClient))

      for (const joinedSocket of joinedSockets.filter(joinedSocket => joinedSocket.id !== socket.id)) {
        for (const producer of joinedSocket.data.producers.values()) {
          createConsumer(
            socket,
            joinedSocket,
            producer,
          )
        }
      }

      socket.broadcast.emit('newPeer', socketToClient(socket))
    })

    socket.on('getRtpCapabilities', (cb) => {
      cb(router.rtpCapabilities)
    })

    socket.on('createTransport', async ({ producing, consuming }, cb) => {
      try {
        const transport = await router.createWebRtcTransport({
          listenInfos: [
            {
              protocol: 'udp',
              ip: '0.0.0.0',
              announcedAddress: process.env.ANNOUNCED_ADDRESS || '127.0.0.1',
              portRange: {
                min: 40000,
                max: 40100,
              },
            },
          ],
          initialAvailableOutgoingBitrate: 8_000_000,
          maxSctpMessageSize: 262144,
          enableUdp: true,
          preferUdp: true,
          appData: {
            producing,
            consuming,
          },
        })

        socket.data.transports.set(transport.id, transport)

        cb({
          id: transport.id,
          iceParameters: transport.iceParameters,
          iceCandidates: transport.iceCandidates,
          dtlsParameters: transport.dtlsParameters,
        })

        transport.on('icestatechange', (iceState: types.IceState) => {
          if (iceState === 'disconnected' || iceState === 'closed') {
            consola.info('[WebRtc]', '[WebRtcTransport]', `"icestatechange" event [iceState:${iceState}], closing peer`, transport.id)

            socket.disconnect()
          }
        })

        transport.on('dtlsstatechange', (dtlsState: types.DtlsState) => {
          if (dtlsState === 'failed' || dtlsState === 'closed') {
            consola.warn('WebRtcTransport "dtlsstatechange" event [dtlsState:%s], closing peer', dtlsState)

            socket.disconnect()
          }
        })
      }
      catch (error) {
        if (error instanceof Error) {
          consola.error('[WebRtc]', '[createTransport]', error.message)
          cb({ error: error.message })
        }
      }
    })

    socket.on('connectTransport', async ({ transportId, dtlsParameters }, cb) => {
      const transport = socket.data.transports.get(transportId)

      if (!transport) {
        consola.error('[WebRtc]', '[connectTransport]', `Transport with id ${transportId} not found`)
        cb({ error: 'Transport not found' })

        return
      }

      try {
        await transport.connect({ dtlsParameters })

        cb({ ok: true })
      }
      catch (error) {
        if (error instanceof Error) {
          consola.error('[WebRtc]', '[connectTransport]', error.message)
          cb({ error: error.message })
        }
      }
    })

    socket.on('produce', async ({ transportId, kind, rtpParameters, appData }, cb) => {
      if (!socket.data.joined) {
        consola.error('Peer not joined yet')
        cb({ error: 'Peer not joined yet' })

        return
      }

      const transport = socket.data.transports.get(transportId)

      if (!transport) {
        consola.error('[WebRtc]', '[produce]', `Transport with id ${transportId} not found`)
        cb({ error: 'Transport not found' })

        return
      }

      try {
        const producer = await (transport as Transport).produce({ kind, rtpParameters, appData: { ...appData, socketId: socket.id } })

        socket.data.producers.set(producer.id, producer)

        cb({ id: producer.id })

        const otherSockets = await getJoinedSockets(socket.id)

        for (const otherSocket of otherSockets) {
          createConsumer(
            otherSocket,
            socket,
            producer,
          )
        }

        await audioLevelObserver.addProducer({ producerId: producer.id })
        await activeSpeakerObserver.addProducer({ producerId: producer.id })
      }
      catch (error) {
        if (error instanceof Error) {
          consola.error('[WebRtc]', '[produce]', error.message)
          cb({ error: error.message })
        }
      }
    })

    socket.on('closeProducer', async ({ producerId }, cb) => {
      if (!socket.data.joined) {
        consola.error('Peer not joined yet')
        cb({ error: 'Peer not joined yet' })

        return
      }

      const producer = socket.data.producers.get(producerId)

      if (!producer) {
        consola.error(`producer with id "${producerId}" not found`)
        cb({ error: `producer with id "${producerId}" not found` })

        return
      }

      producer.close()

      socket.data.producers.delete(producerId)

      cb({ ok: true })
    })

    socket.on('pauseProducer', async ({ producerId }, cb) => {
      if (!socket.data.joined) {
        consola.error('Peer not joined yet')
        cb({ error: 'Peer not joined yet' })

        return
      }

      const producer = socket.data.producers.get(producerId)

      if (!producer) {
        consola.error(`producer with id "${producerId}" not found`)
        cb({ error: `producer with id "${producerId}" not found` })

        return
      }

      if (producer.paused)
        return

      await producer.pause()

      cb({ ok: true })
    })

    socket.on('resumeProducer', async ({ producerId }, cb) => {
      if (!socket.data.joined) {
        consola.error('Peer not joined yet')
        cb({ error: 'Peer not joined yet' })

        return
      }

      const producer = socket.data.producers.get(producerId)

      if (!producer) {
        consola.error(`producer with id "${producerId}" not found`)
        cb({ error: `producer with id "${producerId}" not found` })

        return
      }

      await producer.resume()

      cb({ ok: true })
    })

    socket.on('pauseConsumer', async ({ consumerId }, cb) => {
      if (!socket.data.joined) {
        consola.error('Peer not joined yet')
        cb({ error: 'Peer not joined yet' })

        return
      }

      const consumer = socket.data.consumers.get(consumerId)

      if (!consumer) {
        consola.error(`consumer with id "${consumerId}" not found`)
        cb({ error: `consumer with id "${consumerId}" not found` })

        return
      }

      await consumer.pause()

      cb({ ok: true })
    })

    socket.on('resumeConsumer', async ({ consumerId }, cb) => {
      if (!socket.data.joined) {
        consola.error('Peer not joined yet')
        cb({ error: 'Peer not joined yet' })

        return
      }

      const consumer = socket.data.consumers.get(consumerId)

      if (!consumer) {
        consola.error(`consumer with id "${consumerId}" not found`)
        cb({ error: `consumer with id "${consumerId}" not found` })

        return
      }

      await consumer.resume()

      cb({ ok: true })
    })

    socket.on('updateClient', async (updatedClient, cb) => {
      if (typeof updatedClient.inputMuted === 'boolean') {
        socket.data.inputMuted = updatedClient.inputMuted
      }

      if (typeof updatedClient.outputMuted === 'boolean') {
        socket.data.outputMuted = updatedClient.outputMuted
      }

      cb(socketToClient(socket))

      io.emit('clientChanged', socket.id, socketToClient(socket))
    })

    socket.on('disconnect', () => {
      consola.info('Client disconnected:', socket.id)

      if (socket.data.joined) {
        socket.broadcast.emit('peerClosed', socket.id)
      }

      for (const transport of socket.data.transports.values()) {
        transport.close()
      }
    })
  })

  async function getJoinedSockets(excludeId?: string) {
    const sockets = await io.fetchSockets()

    return sockets.filter(socket => socket.data.joined && (excludeId ? excludeId !== socket.id : true))
  }

  async function createConsumer(
    consumerSocket: SomeSocket,
    producerSocket: SomeSocket,
    producer: types.Producer,
  ) {
    if (
      !consumerSocket.data.rtpCapabilities
      || !router.canConsume(
        {
          producerId: producer.id,
          rtpCapabilities: consumerSocket.data.rtpCapabilities,
        },
      )
    ) {
      return
    }

    const transport = Array.from(consumerSocket.data.transports.values())
      .find(t => t.appData.consuming)

    if (!transport) {
      consola.error('createConsumer() | Transport for consuming not found')

      return
    }

    let consumer: types.Consumer

    try {
      consumer = await transport.consume(
        {
          producerId: producer.id,
          rtpCapabilities: consumerSocket.data.rtpCapabilities,
          enableRtx: true,
          paused: true,
          ignoreDtx: true,
        },
      )
    }
    catch (error) {
      consola.error('_createConsumer() | transport.consume():%o', error)

      return
    }

    consumerSocket.data.consumers.set(consumer.id, consumer)

    consumer.on('transportclose', () => {
      consumerSocket.data.consumers.delete(consumer.id)
    })

    consumer.on('producerclose', () => {
      consumerSocket.data.consumers.delete(consumer.id)

      consumerSocket.emit('consumerClosed', { consumerId: consumer.id })
    })

    consumer.on('producerpause', () => {
      consumerSocket.emit('consumerPaused', { consumerId: consumer.id })
    })

    consumer.on('producerresume', () => {
      consumerSocket.emit('consumerResumed', { consumerId: consumer.id })
    })

    consumer.on('score', (score: types.ConsumerScore) => {
      consumerSocket.emit('consumerScore', { consumerId: consumer.id, score })
    })

    try {
      await consumerSocket.emitWithAck(
        'newConsumer',
        {
          socketId: producerSocket.id,
          producerId: producer.id,
          id: consumer.id,
          kind: consumer.kind,
          rtpParameters: consumer.rtpParameters,
          type: consumer.type,
          appData: producer.appData,
          producerPaused: consumer.producerPaused,
        },
      )

      await consumer.resume()

      consumerSocket.emit(
        'consumerScore',
        {
          consumerId: consumer.id,
          score: consumer.score,
        },
      )
    }
    catch (error) {
      consola.error('_createConsumer() | failed:%o', error)
    }
  }
}
