import { type FC, useEffect, useMemo, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { Navigate, useParams, useSearchParams } from 'react-router-dom';

import { NoiseCancellation } from '@stream-io/audio-filters-web';
import {
  NoiseCancellationProvider,
  StreamCall,
  StreamVideo,
} from '@stream-io/video-react-sdk';
import { type AxiosError } from 'axios';

import leaveCall from 'src/assets/sound/leave-call.mp3';
import { tokenManager } from 'src/config/tokens';
import { EVENTS_SOCKETS } from 'src/constants/sockets';
import { useSocket } from 'src/contexts/SocketContext';
import useGeneralError from 'src/hooks/useGeneralError';
import useHuGoTheme from 'src/hooks/useHuGoTheme';
import { acceptCall, endCall, getVideoCall } from 'src/services/streaming';
import { type ResponseError } from 'src/types/services';
import {
  type CallError,
  type CallEvent,
  CallMode,
  ResponseErrorCodes,
  StatusVideoCall,
  StreamType,
  UserTypeStream,
} from 'src/types/stream';
import { useLokaliseTranslation } from 'src/utils/i18n';

import VideoCall from 'src/components/dashboard/videoCall';
import FallbackMessage from 'src/components/dashboard/videoCall/FallbackMessage';
import { LoadingCall } from 'src/components/dashboard/videoCall/LoadingCall';
import LobbyContainer from 'src/components/dashboard/videoCall/lobby/Container';
import ErrorState from 'src/components/dashboard/videoCall/lobby/ErrorState';

import useGetStreamVideoClient from '../liveStream/hooks/useGetStreamVideoClient';

import { CALL_ERROR_HEADER_KEYS } from './constants';
import { playSoundFromUrl } from './hooks/useConnectionSound';
import { videoCallKeys } from './queries';
import { listenForWindowFocusRequest } from './utils';

const VideoCallView: FC = () => {
  const ThemeProvider = useHuGoTheme();
  const client = useGetStreamVideoClient(UserTypeStream.CALL);
  const showGeneralError = useGeneralError();
  const { t } = useLokaliseTranslation('calls');
  const { callId, providerCallId } = useParams();
  const [searchParams, setSearchParams] = useSearchParams();
  const [callError, setCallError] = useState<CallError | null>(null);
  const [showRingingView, setShowRingingView] = useState(false);
  const socket = useSocket();
  const noiseCancellation = useMemo(() => new NoiseCancellation(), []);

  const newCall = searchParams.get('new');

  const { mutateAsync: acceptCallMutation, isLoading: isLoadingAcceptCall } =
    useMutation(
      (variables: { callId: string }) => acceptCall(variables.callId),
      {
        retry: 2,
        retryDelay: 0,
        onError: (error: AxiosError<ResponseError>) => {
          const { code } = error.response?.data || {};
          if (
            code === ResponseErrorCodes.CALL_INVALID_STATUS ||
            code === ResponseErrorCodes.CALL_TOO_MANY_PARTICIPANTS
          ) {
            setCallError(code);
          } else {
            showGeneralError(error, t('ERROR_ACCEPT_CALL'));
          }
        },
      },
    );

  const { data: providerVideoCall, isLoading: isLoadingProviderVideoCall } =
    useQuery(
      videoCallKeys.providerVideoCall(providerCallId ?? ''),
      async () => {
        const safeCallId = callId ?? '';
        const safeProviderCallId = providerCallId ?? '';
        const callInstance = client.call(
          StreamType.DEFAULT,
          safeProviderCallId,
        );

        if (newCall === 'false') {
          // if new call is false, means that was accepted from incoming call or push notification
          searchParams.delete('new');
          setSearchParams(searchParams, { replace: true });
          await acceptCallMutation({ callId: safeCallId });
          await callInstance.join();
        } else if (newCall) {
          if (newCall === CallMode.SINGLE) {
            setShowRingingView(true); // workaround because stream is not updating the calling state properly
          }
          if (newCall === CallMode.GROUP) {
            // group calls join immediately without waiting for acceptance
            await callInstance.join();
            await acceptCallMutation({ callId: safeCallId });
          }
          searchParams.delete('new');
          setSearchParams(searchParams, { replace: true });
        } else {
          // if new call does not exist, means that the user was joined from link
          // we have to do anything to see the lobby screen (idle state)
          // we just need to get the initial call information, so later we can access it with the hook session
          await callInstance.get();
        }

        return callInstance;
      },
      {
        onError: (error: AxiosError<ResponseError>) => {
          const { code } = error.response?.data || {};
          if (
            code === ResponseErrorCodes.CALL_INVALID_STATUS ||
            code === ResponseErrorCodes.CALL_TOO_MANY_PARTICIPANTS
          ) {
            setCallError(code);
          } else {
            showGeneralError(error, t('ERROR_CREATE_VIDEO_CALL'));
          }
        },
        enabled: !!providerCallId && !!client,
        refetchOnReconnect: false, // To avoid joining twice after a reconnection
      },
    );

  const { data: humandVideoCall, isLoading: isLoadinghumandVideoCall } =
    useQuery(
      videoCallKeys.videoCall(callId ?? ''),
      () => getVideoCall(callId ?? ''),
      {
        select: response => ({
          ...response.data,
          participants: [...response.data.participants, response.data.creator],
        }),
        onSuccess: response => {
          if (response.currentSession.status === StatusVideoCall.ENDED) {
            setCallError(ResponseErrorCodes.CALL_INVALID_STATUS);
          }
        },
        enabled: !!callId,
      },
    );

  useEffect(() => {
    const handleCallStarted = (data: CallEvent) => {
      const callMode = providerVideoCall?.state.custom.type;
      if ((!callMode || callMode === CallMode.SINGLE) && showRingingView) {
        providerVideoCall
          ?.join()
          .then(() => acceptCallMutation({ callId: data.id }))
          .catch(error => showGeneralError(error, t('error_join_call')));
        setShowRingingView(false);
      }
    };

    const handleEndedEvent = (data: CallEvent) => {
      if (callId === data.id) {
        if (showRingingView) {
          window.close();
        } else {
          setCallError(ResponseErrorCodes.FINISHED_CALL);
        }
      }
    };

    socket.listenEvent(EVENTS_SOCKETS.CALL_STARTED, handleCallStarted);
    socket.listenEvent(EVENTS_SOCKETS.CALL_ENDED, handleEndedEvent);

    return () => {
      socket.closeEvent(EVENTS_SOCKETS.CALL_STARTED, handleCallStarted);
      socket.closeEvent(EVENTS_SOCKETS.CALL_ENDED, handleEndedEvent);
    };
  }, [
    socket,
    providerVideoCall,
    providerVideoCall?.state.custom.type,
    showRingingView,
    acceptCallMutation,
    showGeneralError,
    callId,
    t,
  ]);

  useEffect(() => {
    return () => {
      providerVideoCall?.leave();
    };
  }, [providerVideoCall]);

  useEffect(() => {
    if (!callId) return;
    const cleanup = listenForWindowFocusRequest(callId);
    return cleanup;
  }, [callId]);

  if (!providerCallId || !callId) {
    return <Navigate to="/" />;
  }

  const handleEndCall = () => {
    const { accessToken } = tokenManager.getCorrectToken();
    endCall(callId, accessToken);
    setCallError(ResponseErrorCodes.FINISHED_CALL);
    playSoundFromUrl(leaveCall);
  };

  const joinCallFromLobby = async () => {
    await acceptCallMutation({ callId });
    await providerVideoCall
      ?.join()
      .catch(error => showGeneralError(error, t('error_join_call')));
  };

  const participants = humandVideoCall?.participants || [];

  const isLoading = isLoadingProviderVideoCall || isLoadinghumandVideoCall;

  if (callError) {
    return (
      <LobbyContainer title={t(CALL_ERROR_HEADER_KEYS[callError])}>
        <ErrorState error={callError} />
      </LobbyContainer>
    );
  }

  return (
    <ThemeProvider>
      <StreamVideo client={client}>
        <StreamCall call={providerVideoCall}>
          <NoiseCancellationProvider noiseCancellation={noiseCancellation}>
            {isLoading && <LoadingCall />}
            {!isLoading && providerVideoCall && humandVideoCall && (
              <VideoCall
                callId={callId}
                participants={participants}
                joinCallFromLobby={{
                  mutation: joinCallFromLobby,
                  isLoading: isLoadingAcceptCall,
                }}
                initializeVideo={
                  humandVideoCall.initializationConfig.cameraEnabled
                }
                maxParticipantsAllowed={humandVideoCall.maxParticipantsAllowed}
                showRingingView={showRingingView}
                onEndCall={handleEndCall}
              />
            )}
            {!isLoading && !providerVideoCall && !humandVideoCall && (
              <FallbackMessage message={t('ERROR_TO_CONNECT_SERVICE')} />
            )}
          </NoiseCancellationProvider>
        </StreamCall>
      </StreamVideo>
    </ThemeProvider>
  );
};

export default VideoCallView;
