import React, { useEffect, useState, useContext, useCallback } from 'react';
import useRooms from './RoomsProvider';
import useSockets from './SocketProvider';

import MasterLineData from '@nirvana/core/models/masterLineData.model';

import {
  ServerRequestChannels,
  ServerResponseChannels,
  SomeoneConnectedResponse,
  SomeoneDisconnectedResponse,
  SomeoneTunedResponse,
  SomeoneUntunedFromLineResponse,
  TuneToLineRequest,
  UserStartedBroadcastingResponse,
  UserStoppedBroadcastingResponse,
  ConnectToLineRequest,
  UntuneFromLineRequest,
} from '@nirvana/core/sockets/channels';
import toast from 'react-hot-toast';
import { useImmer } from 'use-immer';
import { LineMemberState } from '@nirvana/core/models/line.model';
import { useAsyncFn } from 'react-use';

import { updateLineMemberState } from '../api/NirvanaApi';
import UpdateLineMemberState from '@nirvana/core/requests/updateLineMemberState.request';
import useAuth from './AuthProvider';
import { User } from '@nirvana/core/models/user.model';

type LineIdToMasterLine = {
  [lineId: string]: MasterLineData;
};

// TODO: implement the below to save renders
type TunedMembersMap = {
  [lineId: string]: string[];
};

type ConnectedMembesMap = {
  [lineId: string]: string[];
};

type UserMap = {
  [userId: string]: User;
};

type BroadcastersMap = {
  [lineId: string]: string[];
};

interface IRealTimeRoomProvider {
  roomsMap: LineIdToMasterLine;

  selectedLineId?: string;
  handleSelectLine?: (newLineId: string) => void;

  handleUpdateLineMemberState?: (lineId: string, newState: LineMemberState) => void;
}

const RealTimeRoomContext = React.createContext<IRealTimeRoomProvider>({
  roomsMap: {},
});

/**
 *
 * handles reads of new data
 * keeps listening to incoming socket events to make sure that the realtime rooms map is highly available
 *
 * on load, we want to grab all of the rooms we are in
 * put them in a map
 *
 * fetch more audio clips, fire off async function to fetch more and add to the room map
 *
 * Socket Rooms:
 * - all people online for a line
 * - all tuned in folks on a line...have it selected or toggle tuned
 *
 * Socket Events:
 * - someone connected
 * - someone tuned in
 * - someone started broadcasting
 * - someone stopped broadcasting
 *
 * - someone disconnected...take them out of the necessary lists
 * - someone left x room
 * - someone joined x room
 *
 * - someone added me to line
 *
 * - someone went into flow state their status
 *
 * Socket Emissions:
 * - join a line
 * - tune into a line
 * - send audio clip
 * - create a line -> send to specific people
 *
 * REST endpoints:
 * - toggle tune or untoggle tune
 * - fetch content blocks for line history - react query
 */

export function RealTimeRoomProvider({ children }: { children: React.ReactChild }) {
  const { rooms } = useRooms();
  const { user } = useAuth();
  const { $ws } = useSockets();
  const [roomMap, updateRoomMap] = useImmer<LineIdToMasterLine>({});

  const [selectedLineId, setSelectedLineId] = useState<string>();

  const [moveLineState, moveLine] = useAsyncFn(updateLineMemberState);

  /** All line listeners */
  useEffect(() => {
    // when me or anyone just initially connects to line
    $ws.on(ServerResponseChannels.SOMEONE_CONNECTED_TO_LINE, (res: SomeoneConnectedResponse) => {
      console.log(`${res.userId} connected to room ${res.lineId}`);

      updateRoomMap((draft) => {
        if (!draft[res.lineId]) {
          toast.error('there was a problem updating rooms!!!');
          return;
        }

        draft[res.lineId].connectedMemberIds = res.allUsers;
      });
    });

    // someone tuning in, including perhaps me | either toggled in or just temporary
    $ws.on(ServerResponseChannels.SOMEONE_TUNED_INTO_LINE, (res: SomeoneTunedResponse) => {
      console.log(`${res.userId} tuned into line ${res.lineId}`);

      updateRoomMap((draft) => {
        if (!draft[res.lineId]) {
          toast.error('there was a problem updating rooms!!!');
          return;
        }

        draft[res.lineId].tunedInMemberIds = res.allUsers;
      });
    });

    $ws.on(
      ServerResponseChannels.SOMEONE_UNTUNED_FROM_LINE,
      (res: SomeoneUntunedFromLineResponse) => {
        console.log(`${res.userId} untuned from ${res.lineId}`);

        updateRoomMap((draft) => {
          if (!draft[res.lineId]) {
            toast.error('there was a problem updating rooms!!!');
            return;
          }

          if (draft[res.lineId].tunedInMemberIds) {
            draft[res.lineId].tunedInMemberIds = draft[res.lineId].tunedInMemberIds.filter(
              (userId) => userId !== res.userId,
            );
          }
        });
      },
    );

    // remove them from the line connected list and tuned list if they are there
    $ws.on(
      ServerResponseChannels.SOMEONE_DISCONNECTED_FROM_LINE,
      (res: SomeoneDisconnectedResponse) => {
        updateRoomMap((draft) => {
          if (!draft[res.lineId]) {
            toast.error('there was a problem updating rooms!!!');
            return;
          }

          draft[res.lineId].connectedMemberIds = draft[res.lineId].connectedMemberIds?.filter(
            (userId) => userId !== res.userId,
          );
          draft[res.lineId].tunedInMemberIds = draft[res.lineId].tunedInMemberIds?.filter(
            (userId) => userId !== res.userId,
          );
        });
      },
    );

    $ws.on(
      ServerResponseChannels.SOMEONE_STARTED_BROADCASTING,
      (res: UserStartedBroadcastingResponse) => {
        console.log(`${res.userId} is starting to broadcast in ${res.lineId}`);

        updateRoomMap((draft) => {
          if (!draft[res.lineId]) {
            toast.error('there was a problem updating rooms!!!');
            return;
          }

          if (draft[res.lineId].currentBroadcastersUserIds) {
            draft[res.lineId].currentBroadcastersUserIds.push(res.userId);
          } else {
            draft[res.lineId].currentBroadcastersUserIds = [res.userId];
          }
        });
      },
    );

    $ws.on(
      ServerResponseChannels.SOMEONE_STOPPED_BROADCASTING,
      (res: UserStoppedBroadcastingResponse) => {
        console.log(`${res.userId} is STOPPED BROADCASTING in ${res.lineId}`);

        updateRoomMap((draft) => {
          if (!draft[res.lineId]) {
            toast.error('there was a problem updating rooms!!!');
            return;
          }

          if (draft[res.lineId].currentBroadcastersUserIds) {
            draft[res.lineId].currentBroadcastersUserIds = draft[
              res.lineId
            ].currentBroadcastersUserIds.filter((userId) => userId !== res.userId);
          }
        });
      },
    );

    return () => {
      // ?perhaps only remove specific ones?
      // !this will remove all listeners across the app and we want it to?
      $ws.removeAllListeners();
    };
  }, [$ws, updateRoomMap]);

  const handleConnectToLine = useCallback(
    (lineId: string) => {
      $ws.emit(ServerRequestChannels.CONNECT_TO_LINE, new ConnectToLineRequest(lineId));
    },
    [$ws],
  );

  const handleTuneIntoLine = useCallback(
    (lineId: string) => {
      $ws.emit(ServerRequestChannels.TUNE_INTO_LINE, new TuneToLineRequest(lineId));
    },
    [$ws],
  );

  const handleUntuneFromLine = useCallback(
    (lineId: string) => {
      $ws.emit(ServerRequestChannels.UNTUNE_FROM_LINE, new UntuneFromLineRequest(lineId));
    },
    [$ws],
  );

  // converts the initial rooms fetch to a map
  useEffect(() => {
    if (rooms.value?.data?.masterLines?.length > 0) {
      updateRoomMap((draft) => {
        rooms.value.data.masterLines.forEach((masterLine) => {
          const lineId = masterLine.lineDetails._id.toString();
          draft[lineId] = masterLine;

          handleConnectToLine(lineId);
          if (masterLine.currentUserMember.state === LineMemberState.TUNED) {
            handleTuneIntoLine(lineId);
          }
        });
      });
    }
  }, [rooms.value, updateRoomMap, handleConnectToLine, handleTuneIntoLine]);

  // persist whether I want it toggle tuned or not
  const handleUpdateLineMemberState = useCallback(
    (lineId: string, newState: LineMemberState) => {
      moveLine(new UpdateLineMemberState(newState), lineId)
        .then((_res) => {
          updateRoomMap((draft) => {
            draft[lineId].currentUserMember.state = newState;
          });
        })
        .catch((error) => {
          toast.error('problem in updating line member state');
          console.error(error);
        });
    },
    [moveLine, updateRoomMap],
  );

  /**
   * selection globally
   * tune into socket room and tell everyone else
   */
  const handleSelectLine = useCallback(
    (newLineIdToSelect: string) => {
      setSelectedLineId((prevLineId) => {
        if (newLineIdToSelect === prevLineId) {
          return prevLineId;
        }

        // untune from the last line if it was just a temporary tuned one
        if (roomMap[prevLineId]?.currentUserMember.state === LineMemberState.INBOX) {
          handleUntuneFromLine(prevLineId);
        }

        // tune in if not already tuned into this line
        if (!roomMap[newLineIdToSelect].tunedInMemberIds?.includes(user._id.toString())) {
          handleTuneIntoLine(newLineIdToSelect);
        }

        return newLineIdToSelect;
      });
    },
    [setSelectedLineId, handleUntuneFromLine, roomMap, user, handleTuneIntoLine],
  );

  return (
    <RealTimeRoomContext.Provider
      value={{ roomsMap: roomMap, handleSelectLine, selectedLineId, handleUpdateLineMemberState }}
    >
      {children}
    </RealTimeRoomContext.Provider>
  );
}

export default function useRealTimeRooms() {
  return useContext(RealTimeRoomContext);
}
