import type { ChadClient } from '#shared/types'
import { createSharedComposable } from '@vueuse/core'
import * as mediasoupClient from 'mediasoup-client'
import { usePreferences } from '~/composables/use-preferences'
import { useSignaling } from '~/composables/use-signaling'

const ICE_SERVERS: RTCIceServer[] = [
  { urls: 'stun:stun.l.google.com:19302' },
  { urls: 'stun:stun.l.google.com:5349' },
  { urls: 'stun:stun1.l.google.com:3478' },
  { urls: 'stun:stun1.l.google.com:5349' },
  { urls: 'stun:stun2.l.google.com:19302' },
  { urls: 'stun:stun2.l.google.com:5349' },
  { urls: 'stun:stun3.l.google.com:3478' },
  { urls: 'stun:stun3.l.google.com:5349' },
  { urls: 'stun:stun4.l.google.com:19302' },
  { urls: 'stun:stun4.l.google.com:5349' },
]

export const useMediasoup = createSharedComposable(() => {
  const toast = useToast()

  const signaling = useSignaling()
  const { addClient, removeClient } = useClients()
  const preferences = usePreferences()
  const { me } = useAuth()

  const device = shallowRef<mediasoupClient.Device>()
  const rtpCapabilities = shallowRef<mediasoupClient.types.RtpCapabilities>()
  const sendTransport = shallowRef<mediasoupClient.types.Transport>()
  const recvTransport = shallowRef<mediasoupClient.types.Transport>()

  const micProducer = shallowRef<mediasoupClient.types.Producer>()
  const webcamProducer = shallowRef<mediasoupClient.types.Producer>()
  const shareProducer = shallowRef<mediasoupClient.types.Producer>()

  const consumers = shallowRef<Map<string, mediasoupClient.types.Consumer>>(new Map())
  const producers = shallowRef<Map<string, mediasoupClient.types.Producer>>(new Map())

  watch(signaling.socket, (socket) => {
    if (!socket)
      return

    socket.on('authenticated', async () => {
      if (!signaling.socket.value)
        return

      device.value = new mediasoupClient.Device()
      rtpCapabilities.value = await signaling.socket.value.emitWithAck('getRtpCapabilities')

      await device.value.load({ routerRtpCapabilities: rtpCapabilities.value! })

      // Send transport
      {
        const transportInfo = await signaling.socket.value.emitWithAck('createTransport', { producing: true, consuming: false })
        sendTransport.value = device.value.createSendTransport({
          ...transportInfo,
          iceServers: [
            ...ICE_SERVERS,
            ...(transportInfo.iceServers ?? []),
          ],
        })

        sendTransport.value.on('connect', async ({ dtlsParameters }, callback, errback) => {
          try {
            await signaling.socket.value!.emitWithAck('connectTransport', {
              transportId: sendTransport.value!.id,
              dtlsParameters,
            })

            callback()
          }
          catch (error) {
            if (error instanceof Error) {
              errback(error)
            }
          }
        })

        sendTransport.value.on('produce', async ({ kind, rtpParameters, appData }, callback, errback) => {
          try {
            const { id } = await signaling.socket.value!.emitWithAck('produce', {
              transportId: sendTransport.value!.id,
              kind,
              rtpParameters,
              appData,
            })
            callback({ id })
          }
          catch (error) {
            if (error instanceof Error) {
              errback(error)
            }
          }
        })
      }

      // Recv Transport
      {
        const transportInfo = await signaling.socket.value.emitWithAck('createTransport', { producing: false, consuming: true })
        recvTransport.value = device.value.createRecvTransport({
          ...transportInfo,
          iceServers: [
            ...ICE_SERVERS,
            ...(transportInfo.iceServers ?? []),
          ],
        })

        recvTransport.value.on('connect', async ({ dtlsParameters }, callback, errback) => {
          try {
            await signaling.socket.value!.emitWithAck('connectTransport', {
              transportId: recvTransport.value!.id,
              dtlsParameters,
            })

            callback()
          }
          catch (error) {
            if (error instanceof Error) {
              errback(error)
            }
          }
        })
      }

      const joinedClients = (await signaling.socket.value.emitWithAck('join', {
        rtpCapabilities: rtpCapabilities.value,
      }))

      addClient(...joinedClients)

      toast.add({ severity: 'success', summary: 'Joined', closable: false, life: 1000 })

      await enableMic()
    })

    socket.on('newPeer', (client) => {
      addClient(client)
    })

    socket.on('peerClosed', (id) => {
      removeClient(id)
    })

    socket.on(
      'newConsumer',
      async (
        { id, producerId, kind, rtpParameters, peerId: clientId, appData },
        cb,
      ) => {
        if (!recvTransport.value)
          return

        const consumer = await recvTransport.value.consume({
          id,
          producerId,
          kind,
          rtpParameters,
          streamId: `${clientId}-${appData.share ? 'share' : 'mic-webcam'}`,
          appData: { ...appData, clientId },
        })

        consumer.on('transportclose', () => {
          consumers.value.delete(consumer.id)
          triggerRef(consumers)
        })

        consumers.value.set(consumer.id, consumer)
        triggerRef(consumers)

        cb()
      },
    )

    socket.on(
      'consumerClosed',
      async (
        { consumerId },
      ) => {
        const consumer = consumers.value.get(consumerId)

        if (!consumer)
          return

        consumers.value.delete(consumer.id)
        triggerRef(consumers)
      },
    )

    socket.on('disconnect', () => {
      device.value = undefined
      rtpCapabilities.value = undefined

      sendTransport.value?.close()
      sendTransport.value = undefined

      recvTransport.value?.close()
      recvTransport.value = undefined

      micProducer.value = undefined
      webcamProducer.value = undefined
      shareProducer.value = undefined

      consumers.value = new Map()
      producers.value = new Map()
    })
  }, { immediate: true, flush: 'sync' })

  function getClientConsumers(clientId: ChadClient['socketId']) {
    return consumers.value.values().filter(consumer => consumer.appData.clientId === clientId)
  }

  async function enableMic() {
    if (micProducer.value)
      return

    if (!device.value || !sendTransport.value)
      return

    if (!device.value.canProduce('audio'))
      return

    const stream = await navigator.mediaDevices.getUserMedia({
      audio: {
        autoGainControl: false,
        noiseSuppression: true,
        echoCancellation: false,
        channelCount: 2,
      },
    })
    const track = stream.getAudioTracks()[0]

    if (!track)
      return

    micProducer.value = await sendTransport.value.produce({
      track,
      codecOptions: {
        opusStereo: true,
        opusDtx: true, // Меньше пакетов летит когда тишина
        opusFec: false, // Фиксит пакет лос
      },
    })

    producers.value.set(micProducer.value.id, micProducer.value)
    triggerRef(producers)

    micProducer.value.on('transportclose', () => {
      micProducer.value = undefined
    })

    micProducer.value.on('trackended', () => {
      disableMic()
    })
  }

  async function disableMic() {
    if (!signaling.socket.value || !micProducer.value)
      return

    producers.value.delete(micProducer.value.id)
    triggerRef(producers)

    try {
      micProducer.value.close()

      await signaling.socket.value.emitWithAck('closeProducer', {
        producerId: micProducer.value.id,
      })
    }
    catch {
    }

    micProducer.value = undefined
  }

  async function muteMic() {
    if (!signaling.socket.value || !micProducer.value)
      return

    try {
      micProducer.value.pause()

      await signaling.socket.value.emitWithAck('pauseProducer', {
        producerId: micProducer.value.id,
      })
    }
    catch {
    }
  }

  async function unmuteMic() {
    if (!signaling.socket.value || !micProducer.value)
      return

    try {
      micProducer.value.resume()

      await signaling.socket.value.emitWithAck('resumeProducer', {
        producerId: micProducer.value.id,
      })
    }
    catch {
    }
  }

  async function init() {
    signaling.connect()
  }

  return {
    init,
    consumers,
    producers,
    sendTransport,
    recvTransport,
    rtpCapabilities,
    device,
    micProducer,
    webcamProducer,
    shareProducer,
    getClientConsumers,
    muteMic,
    unmuteMic,
  }
})
