import {
  ChatMessage,
  ConnectionQuality,
  LocalParticipant,
  Participant,
  RemoteParticipant,
  RoomInfo,
  Session,
  SessionEvent,
  SessionState,
  StreamKind,
  StreamSource,
  Track,
  TranscriptionConfig,
  TranscriptionResult,
  VirtualBackgroundConfig,
  VirtualBackgroundType,
  ZocksSDK,
} from '@zocks-communications/client-sdk-js'
import { useZocksDispatch, useZocksStore } from '../../app/hooks'
import {
  activeSpeakersChanged,
  addRemoteParticipant,
  clearParticipants,
  clearRoomId,
  clearRoomName,
  connectionQualityChanged,
  participantLocalTrackPublished,
  participantLocalTrackUnPublished,
  participantTrackMuted,
  participantTrackPublished,
  participantTrackSubscribed,
  participantTrackUnMuted,
  participantTrackUnPublished,
  participantTrackUnsubscribed,
  removeRemoteParticipant,
  selectActiveSpeakers,
  selectIsConnecting,
  selectLocalParticipant,
  selectLocalParticipantIsConnected,
  selectRemoteParticipants,
  selectRoomId,
  selectRoomName,
  selectSessionState,
  selectTracksForParticipant,
  setConnecting,
  setLocalParticipantId,
  setLocalParticipantName,
  setRoomId,
  setRoomName,
  setSessionState,
} from './sessionSlice'
import { useEffect, useState } from 'react'
import { ZocksConfig } from '../../models'
import {
  clearChatMessages,
  privateChatMessageReceived,
  privateChatMessageSent,
  publicChatMessageReceived,
  publicChatMessageSent,
} from '../chat/chatSlice'
import { ChatAttachmentModel } from '../chat'
import { getSessionByRoomId } from '../utils'
import { JoinOptions, ParticipantModel, ParticipantView, SessionView, ZocksActions } from './models'
import { clearCurrentVirtualBackground } from '../virtualBackground/virtualBackgroundSlice'
import {
  clearRemoteParticipantTranscription,
  clearTranscription,
  setTranscriptionInProgress,
  setTranscriptionServiceEnabled,
  setTranscriptionServiceIsLoading,
  transcriptionReceived,
} from '../transcript/transcriptSlice'

export const useZocksActions = (zocksConfig: ZocksConfig): ZocksActions => {
  const dispatch = useZocksDispatch()

  const [session, setSession] = useState<Session>()

  const setupEventHandlers = (session: Session) => {
    session.on(SessionEvent.ParticipantLocalTrackPublished, (track: Track) => {
      if (track.isAvailable) {
        dispatch(
          participantLocalTrackPublished({
            trackId: track.id!,
            trackKind: track.kind,
            trackSource: track.source,
            trackIsMuted: track.isMuted,
          })
        )
      }
    })
    session.on(SessionEvent.ParticipantLocalTrackUnpublished, (track: Track) => {
      if (track.isAvailable) {
        dispatch(
          participantLocalTrackUnPublished({
            trackId: track.id!,
            trackKind: track.kind,
            trackSource: track.source,
          })
        )
      }
    })
    session.on(SessionEvent.ParticipantTrackPublished, (track: Track, participant: Participant) => {
      if (track.isAvailable) {
        dispatch(
          participantTrackPublished({
            participantId: participant.id,
            trackId: track.id,
            trackKind: track.kind,
            trackSource: track.source,
            trackIsMuted: track.isMuted,
            trackIsSubscribed: track.isSubscribed,
          })
        )
      }
    })
    session.on(SessionEvent.ParticipantTrackUnpublished, (track: Track, participant: Participant) => {
      if (track.isAvailable) {
        dispatch(
          participantTrackUnPublished({
            participantId: participant.id,
            trackId: track.id!,
            trackKind: track.kind,
            trackSource: track.source,
          })
        )
      }
    })
    session.on(SessionEvent.SessionConnected, () => {
      dispatch(setRoomId(session.room.id ?? ''))
      dispatch(setRoomName(session.room.name ?? ''))
    })
    session.on(SessionEvent.SessionStateChanged, (sessionState: SessionState) => {
      dispatch(setSessionState(sessionState))
      if (sessionState === SessionState.DISCONNECTED) {
        dispatch(clearParticipants())
        dispatch(clearRoomId())
        dispatch(clearRoomName())
        dispatch(clearChatMessages())
        dispatch(clearCurrentVirtualBackground())
        dispatch(clearTranscription())
      }
    })
    session.on(SessionEvent.LocalParticipantConnected, (localParticipant: Participant) => {
      dispatch(setLocalParticipantId(localParticipant.id))
      dispatch(setLocalParticipantName(localParticipant.name))
    })
    session.on(SessionEvent.ParticipantConnected, (participant: RemoteParticipant) => {
      dispatch(addRemoteParticipant({ participantId: participant.id, participantName: participant.name }))
    })
    session.on(SessionEvent.ParticipantDisconnected, (participantId: string | undefined) => {
      if (participantId) {
        dispatch(removeRemoteParticipant({ participantId }))
        dispatch(clearRemoteParticipantTranscription({ participantId }))
      }
    })
    session.on(SessionEvent.ParticipantTrackSubscribed, (track: Track, participant: RemoteParticipant) => {
      if (participant.id && track.isAvailable) {
        dispatch(
          participantTrackSubscribed({
            participantId: participant.id,
            trackId: track.id!,
            trackIsMuted: track.isMuted,
          })
        )
      }
    })
    session.on(SessionEvent.ParticipantTrackUnsubscribed, (track: Track, participant: RemoteParticipant) => {
      if (participant.id && track.isAvailable) {
        dispatch(participantTrackUnsubscribed({ participantId: participant.id, trackId: track.id! }))
      }
    })
    session.on(
      SessionEvent.ConnectionQualityChanged,
      (connectionQuality: ConnectionQuality, participant: LocalParticipant | RemoteParticipant) => {
        dispatch(connectionQualityChanged({ participantId: participant.id, connectionQuality }))
      }
    )
    session.on(SessionEvent.ParticipantTrackMuted, (track: Track, participant: Participant) => {
      dispatch(participantTrackMuted({ participantId: participant.id, trackId: track.id }))
    })
    session.on(SessionEvent.ParticipantTrackUnmuted, (track: Track, participant: Participant) => {
      dispatch(participantTrackUnMuted({ participantId: participant.id, trackId: track.id }))
    })
    session.on(SessionEvent.ActiveSpeakersChanged, (participants: Participant[]) => {
      dispatch(activeSpeakersChanged(participants.filter((p) => !!p.id).map((p) => p.id)))
    })

    session.on(SessionEvent.PrivateChatMessageReceived, (chatMessage: ChatMessage) => {
      const { message, timestamp, from, attachment } = chatMessage
      dispatch(
        privateChatMessageReceived({
          message,
          timestamp,
          from,
          to: {
            id: session.localParticipant.id,
            name: session.localParticipant.name!,
          },
          attachment,
        })
      )
    })
    session.on(SessionEvent.PublicChatMessageReceived, (chatMessage: ChatMessage) => {
      const { message, timestamp, from, attachment } = chatMessage

      dispatch(
        publicChatMessageReceived({
          message,
          timestamp,
          from,
          attachment,
        })
      )
    })

    session.on(SessionEvent.LocalTranscriptionStarted, () => {
      dispatch(setTranscriptionInProgress(true))
    })

    session.on(SessionEvent.LocalTranscriptionFinished, () => {
      dispatch(setTranscriptionInProgress(false))
    })

    session.on(
      SessionEvent.TranscriptionReceived,
      (transcriptionResult: TranscriptionResult, participant: Participant) => {
        dispatch(transcriptionReceived({ participantId: participant.id, transcriptionResult }))
      }
    )
  }

  const createRoom = (): Promise<RoomInfo> => {
    const zocksSDK = ZocksSDK.init(zocksConfig)
    return zocksSDK.API.media.createRoom()
  }

  const join = async (joinOptions: JoinOptions) => {
    const {
      roomId,
      roomName,
      roomConfig,
      password,
      autoCreate,
      autoSubscribe,
      audioDeviceSettings,
      videoDeviceSettings,
      virtualBackground,
      virtualBackgroundConfig,
      afterJoinCallback,
      joinErrorCallback,
    } = joinOptions
    const zocksSDK = ZocksSDK.init(zocksConfig)

    const zocksSession = zocksSDK.createNewSession()
    setSession(zocksSession)
    setupEventHandlers(zocksSession)

    try {
      dispatch(setConnecting(true))
      await zocksSession.join(roomId, autoCreate, { autoSubscribe }, roomConfig, password, roomName)
      afterJoinCallback && afterJoinCallback()
      if (audioDeviceSettings) {
        await zocksSession.localParticipant.publishAudio(audioDeviceSettings)
      }
      if (virtualBackground) {
        zocksSession.localParticipant.setVirtualBackground(
          virtualBackground,
          virtualBackgroundConfig,
          videoDeviceSettings
        )
      } else if (videoDeviceSettings) {
        await zocksSession.localParticipant.publishVideo(videoDeviceSettings)
      }
    } catch (e: any) {
      joinErrorCallback && joinErrorCallback(e)
    } finally {
      dispatch(setConnecting(false))
    }
  }

  const leave = () => {
    if (session?.localParticipant.hasVirtualBackground()) {
      session?.localParticipant.clearVirtualBackground(false)
    }
    session?.disconnect()
  }

  const publishVideo = (mediaTrackSettings?: MediaTrackSettings) => {
    session?.localParticipant.publishVideo(mediaTrackSettings)
  }

  const unPublishVideo = (mediaTrackSettings?: MediaTrackSettings) => {
    session?.localParticipant.unpublishVideo(mediaTrackSettings)
  }

  const publishAudio = (mediaTrackSettings?: MediaTrackSettings) => {
    session?.localParticipant.publishAudio(mediaTrackSettings)
  }

  const unPublishAudio = (mediaTrackSettings?: MediaTrackSettings) => {
    session?.localParticipant.unpublishAudio(mediaTrackSettings)
  }

  const publishScreenShare = () => {
    session?.localParticipant.publishScreenShare().catch(console.error)
  }

  const unPublishScreenShare = () => {
    session?.localParticipant.unpublishScreenShare()
  }

  const switchActiveDevice = (kind: MediaDeviceKind, deviceId: string) => {
    session?.switchActiveDevice(kind, deviceId)
  }

  const setVirtualBackground = async (
    type: VirtualBackgroundType,
    config?: VirtualBackgroundConfig,
    mediaTrackSettings?: MediaTrackSettings
  ) => {
    await session?.localParticipant.setVirtualBackground(type, config, mediaTrackSettings)
  }

  const clearVirtualBackground = async (mediaTrackSettings?: MediaTrackSettings) => {
    await session?.localParticipant.clearVirtualBackground(true, mediaTrackSettings)
  }

  const sendPublicMessage = (message: string, attachment?: ChatAttachmentModel) => {
    if (!session) {
      throw Error('Cannot send public message without a session!')
    }
    session.sendMessageToRoom(message, attachment)
    dispatch(
      publicChatMessageSent({
        message,
        timestamp: Date.now(),
        from: {
          id: session.localParticipant.id,
          name: session.localParticipant.name!,
        },
        attachment,
      })
    )
  }

  const sendPrivateMessage = (
    message: string,
    remoteParticipant: ParticipantModel,
    attachment?: ChatAttachmentModel
  ) => {
    if (!remoteParticipant.id || !session) {
      throw Error('Cannot send private message without a session or recipient!')
    }
    session.sendMessageToParticipant(message, remoteParticipant.id, attachment)
    dispatch(
      privateChatMessageSent({
        message,
        timestamp: Date.now(),
        from: {
          id: session.localParticipant.id,
          name: session.localParticipant.name!,
        },
        to: {
          id: remoteParticipant.id,
          name: remoteParticipant.name!,
        },
        attachment,
      })
    )
  }

  const uploadAttachment = async (file: File, remoteParticipant?: ParticipantModel) => {
    if ((remoteParticipant && !remoteParticipant.id) || !session) {
      throw Error('Cannot upload attachment without a session or with empty recipient id!')
    }
    const recipientId = remoteParticipant?.id || undefined
    return session.uploadAttachment(file, recipientId)
  }

  const getAttachmentLocation = async (attachmentFileId: string) => {
    if (!session) {
      throw Error('Cannot get attachment location without a session!')
    }
    return session.getFileDownloadLocation(attachmentFileId)
  }

  const enableTranscription = async (config?: TranscriptionConfig) => {
    dispatch(setTranscriptionServiceIsLoading(true))
    try {
      await session?.transcriptionService.enable(config)
      dispatch(setTranscriptionServiceEnabled(true))
    } finally {
      dispatch(setTranscriptionServiceIsLoading(false))
    }
  }

  const disableTranscription = async () => {
    dispatch(setTranscriptionServiceIsLoading(true))
    try {
      await session?.transcriptionService.disable()
      dispatch(setTranscriptionServiceEnabled(false))
    } finally {
      dispatch(setTranscriptionServiceIsLoading(false))
    }
  }

  const startTranscription = async (audioConstraints?: MediaTrackConstraints) => {
    await session?.transcriptionService.startTranscription(audioConstraints)
  }

  const stopTranscription = async () => {
    await session?.transcriptionService.stopTranscription()
  }

  return {
    session,
    sessionActions: {
      createRoom,
      join,
      leave,
    },
    localMediaActions: {
      publishVideo,
      publishAudio,
      unPublishVideo,
      unPublishAudio,
      publishScreenShare,
      unPublishScreenShare,
      switchActiveDevice,
      setVirtualBackground,
      clearVirtualBackground,
    },
    chatActions: {
      sendPublicMessage,
      sendPrivateMessage,
      uploadAttachment,
      getAttachmentLocation,
    },
    transcriptionActions: {
      enableTranscription,
      disableTranscription,
      startTranscription,
      stopTranscription,
    },
  }
}

export const useZocksSession = (): SessionView => {
  const roomId = useZocksStore(selectRoomId)
  const roomName = useZocksStore(selectRoomName)
  const sessionState = useZocksStore(selectSessionState)
  const isConnecting = useZocksStore(selectIsConnecting)
  const localParticipant = useZocksStore(selectLocalParticipant)
  const remoteParticipants = useZocksStore(selectRemoteParticipants)
  const localParticipantIsConnected = useZocksStore(selectLocalParticipantIsConnected)

  return {
    roomId,
    roomName,
    sessionState,
    isConnecting,
    localParticipant,
    remoteParticipants,
    localParticipantIsConnected,
  }
}

export const useParticipant = (participantModel: ParticipantModel): ParticipantView => {
  const tracks = useZocksStore((state) => selectTracksForParticipant(state, participantModel))
  const localParticipant = useZocksStore(selectLocalParticipant)
  const activeSpeakers = useZocksStore(selectActiveSpeakers)
  const isLocal = participantModel.id === localParticipant.id
  const isSpeaking = activeSpeakers.some((participantId) => participantId === participantModel.id)
  const isScreenSharing = !!tracks.screenShareTrack && tracks.screenShareTrack.isSubscribed

  const { id, name, connectionQuality } = participantModel

  return {
    id,
    name,
    tracks,
    isLocal,
    connectionQuality,
    isSpeaking,
    isScreenSharing,
  }
}

export const useLeaveOnUnload = (leave: () => void) => {
  useEffect(() => {
    window.onunload = () => {
      leave()
    }
  }, [leave])
}

export const useSynchronizeParticipants = () => {
  const dispatch = useZocksDispatch()
  const stateParticipants = useZocksStore(selectRemoteParticipants)
  const sessionId = useZocksStore(selectRoomId)
  const zocksSession = getSessionByRoomId(sessionId)

  const synchronizeParticipants = () => {
    if (!zocksSession) {
      return
    }
    const { participants: zocksParticipants } = zocksSession
    for (const stateParticipant of stateParticipants) {
      if (stateParticipant.id && !zocksParticipants.has(stateParticipant.id)) {
        removeObsoleteParticipant(stateParticipant.id)
      }
    }

    for (const zocksParticipant of zocksParticipants.values()) {
      const { id: zocksParticipantId, name: zocksParticipantName, tracks: zocksParticipantTracks } = zocksParticipant
      const stateParticipant = stateParticipants.find((participant) => participant.id === zocksParticipantId)
      if (!stateParticipant) {
        addMissingParticipant(zocksParticipantTracks, zocksParticipantId, zocksParticipantName)
      } else {
        synchronizeExistingParticipant(stateParticipant, zocksParticipantTracks)
      }
    }
  }

  const removeObsoleteParticipant = (participantId: string) => {
    dispatch(removeRemoteParticipant({ participantId }))
  }

  const addMissingParticipant = (participantTracks: Track[], participantId: string, participantName?: string) => {
    dispatch(addRemoteParticipant({ participantId, participantName }))
    for (const zocksTrack of participantTracks) {
      addTrack(participantId, zocksTrack)
    }
  }

  const synchronizeExistingParticipant = (stateParticipant: ParticipantModel, zocksTracks: Track[]) => {
    fixMuteAndSubscriptionStatusOfTracks(stateParticipant, zocksTracks)
    addMissingTracks(stateParticipant, zocksTracks)
    removeObsoleteTracks(stateParticipant, zocksTracks)
  }

  const fixMuteAndSubscriptionStatusOfTracks = (stateParticipant: ParticipantModel, zocksTracks: Track[]) => {
    const {
      tracks: { videoTracks, audioTracks },
    } = stateParticipant
    for (const stateVideoTrack of videoTracks) {
      const zocksVideoTrack = zocksTracks.find((t) => t.id === stateVideoTrack.id)
      if (zocksVideoTrack) {
        if (stateVideoTrack.isSubscribed !== zocksVideoTrack.isSubscribed) {
          changeSubscribedStatusOfTrack(
            stateParticipant.id!,
            stateVideoTrack.id,
            zocksVideoTrack.isSubscribed,
            zocksVideoTrack.isMuted
          )
        }
        if (stateVideoTrack.isMuted !== zocksVideoTrack.isMuted) {
          changeMuteStatusOfTrack(stateParticipant.id!, stateVideoTrack.id, zocksVideoTrack.isMuted)
        }
      }
    }
    for (const stateAudioTrack of audioTracks) {
      const zocksAudioTrack = zocksTracks.find((t) => t.id === stateAudioTrack.id)
      if (zocksAudioTrack) {
        if (stateAudioTrack.isSubscribed !== zocksAudioTrack.isSubscribed) {
          changeSubscribedStatusOfTrack(
            stateParticipant.id!,
            stateAudioTrack.id,
            zocksAudioTrack.isSubscribed,
            zocksAudioTrack.isMuted
          )
        }
        if (stateAudioTrack.isMuted !== zocksAudioTrack.isMuted) {
          changeMuteStatusOfTrack(stateParticipant.id!, stateAudioTrack.id, zocksAudioTrack.isMuted)
        }
      }
    }
  }

  const addMissingTracks = (stateParticipant: ParticipantModel, zocksParticipantTracks: Track[]) => {
    const {
      id,
      tracks: { videoTracks, audioTracks, screenShareTrack },
    } = stateParticipant

    for (const zocksTrack of zocksParticipantTracks) {
      const trackIsMissingFromState =
        !videoTracks.some((t) => t.id === zocksTrack.id) &&
        !audioTracks.some((t) => t.id === zocksTrack.id) &&
        screenShareTrack?.id !== zocksTrack.id
      if (trackIsMissingFromState) {
        addTrack(id!, zocksTrack)
      }
    }
  }

  const removeObsoleteTracks = (stateParticipant: ParticipantModel, zocksTracks: Track[]) => {
    const {
      tracks: { videoTracks, audioTracks, screenShareTrack },
    } = stateParticipant
    for (const stateVideoTrack of videoTracks) {
      if (!zocksTracks.some((t) => t.id === stateVideoTrack.id)) {
        removeTrack(stateParticipant.id!, stateVideoTrack.id, StreamSource.CAMERA, StreamKind.VIDEO)
      }
    }
    for (const stateAudioTrack of audioTracks) {
      if (!zocksTracks.some((t) => t.id === stateAudioTrack.id)) {
        removeTrack(stateParticipant.id!, stateAudioTrack.id, StreamSource.MICROPHONE, StreamKind.AUDIO)
      }
    }
    if (screenShareTrack && !zocksTracks.some((t) => t.id === screenShareTrack.id)) {
      removeTrack(stateParticipant.id!, screenShareTrack.id, StreamSource.SCREEN_SHARE, StreamKind.VIDEO)
    }
  }

  const addTrack = (participantId: string, track: Track) => {
    dispatch(
      participantTrackPublished({
        participantId: participantId,
        trackId: track.id,
        trackKind: track.kind,
        trackSource: track.source,
        trackIsMuted: track.isMuted,
        trackIsSubscribed: track.isSubscribed,
      })
    )
  }

  const removeTrack = (participantId: string, trackId: string, trackSource: StreamSource, trackKind: StreamKind) => {
    dispatch(participantTrackUnPublished({ participantId, trackId, trackSource, trackKind }))
  }

  const changeSubscribedStatusOfTrack = (
    participantId: string,
    trackId: string,
    shouldSubscribe: boolean,
    trackIsMuted: boolean
  ) => {
    if (shouldSubscribe) {
      dispatch(participantTrackSubscribed({ participantId: participantId, trackId: trackId, trackIsMuted }))
    } else {
      dispatch(participantTrackUnsubscribed({ participantId: participantId, trackId: trackId }))
    }
  }

  const changeMuteStatusOfTrack = (participantId: string, trackId: string, shouldMute: boolean) => {
    if (shouldMute) {
      dispatch(participantTrackMuted({ participantId, trackId }))
    } else {
      dispatch(participantTrackUnMuted({ participantId, trackId }))
    }
  }

  return { synchronizeParticipants }
}
