import {
  createContext,
  useCallback,
  useEffect,
  useMemo,
  useReducer,
  useRef,
  useState
} from 'react';
import PropTypes from 'prop-types';
import { useLocation } from 'react-router-dom';

import { channel as $channelContent } from '../content';
import { useUser } from './User';
import useContextHook from './useContextHook';
import { useChannel } from './Channel';
import { useNotif } from './Notification';
import useChatActions from '../pages/Channel/Chat/useChatConnection/useChatActions';
import {
  CHAT_LOG_LEVELS,
  MAX_RECONNECT_ATTEMPTS,
  STREAM_ACTION_NAME
} from '../constants';
import { ivsChatWebSocketRegionOrUrl } from '../api/utils';
import {
  CHAT_USER_ROLE,
  requestChatToken
} from '../pages/Channel/Chat/useChatConnection/utils';
import { ChatRoom } from 'amazon-ivs-chat-messaging';
import {
  extractChannelIdfromChannelArn,
  updateVotes,
  isVotingBlocked
} from '../utils';
import { usePoll } from './StreamManagerActions/Poll';
import { CHAT_MESSAGE_EVENT_TYPES } from '../constants';

const {
  SEND_MESSAGE,
  START_POLL,
  END_POLL,
  SUBMIT_VOTE,
  SEND_VOTE_STATS,
  HEART_BEAT
} = CHAT_MESSAGE_EVENT_TYPES;

const $content = $channelContent.chat;

const { INFO: info, DEBUG: debug } = CHAT_LOG_LEVELS;

const Context = createContext(null);
Context.displayName = 'Chat';

/**
 * @typedef {Object} SenderAttributes
 * @property {string} avatar
 * @property {string} color
 * @property {string} displayName
 *
 * @typedef {Object} Sender
 * @property {SenderAttributes} Attributes
 * @property {string} UserId
 *
 * @typedef {Object} Message
 * @property {object|null=} Attributes
 * @property {string} Content
 * @property {string} Id
 * @property {string=} RequestId
 * @property {Date} SendTime
 * @property {Sender} Sender
 * @property {string} Type
 *
 * @typedef {Array<Message>} Messages
 */

const actionTypes = {
  INIT_MESSAGES: 'INIT_MESSAGES',
  ADD_MESSAGE: 'ADD_MESSAGE',
  DELETE_MESSAGE: 'DELETE_MESSAGE',
  DELETE_MESSAGES_BY_USER_ID: 'DELETE_MESSAGES_BY_USER_ID'
};

const reducer = (messages, action) => {
  switch (action.type) {
    case actionTypes.INIT_MESSAGES: {
      return action.initialMessages || [];
    }
    case actionTypes.ADD_MESSAGE: {
      const { message: newMessage, isOwnMessage } = action;

      return [...messages, { ...newMessage, isOwnMessage }];
    }
    case actionTypes.DELETE_MESSAGE: {
      const { messageId: messageIdToDelete, deletedMessageIds } = action;
      const wasDeletedByUser =
        deletedMessageIds.current.includes(messageIdToDelete);

      const newMessages = messages.reduce(
        (acc, msg) => [
          ...acc,
          msg.id === messageIdToDelete
            ? { ...msg, isDeleted: true, wasDeletedByUser }
            : msg
        ],
        []
      );

      return newMessages;
    }
    case actionTypes.DELETE_MESSAGES_BY_USER_ID: {
      const { userId: userIdToDelete } = action;

      const newMessages = messages.filter(
        (msg) => msg.sender.attributes.channelArn !== userIdToDelete
      );

      return newMessages;
    }
    default:
      throw new Error('Unexpected action type');
  }
};

export const Provider = ({ children }) => {
  /** @type {[Messages, Function]} */
  const [messages, dispatch] = useReducer(reducer, []);

  /**
   * `sentMessageIds` and `deletedMessageIds` have to be refs to avoid redefining `addMessage` which would reset the chat connection.
   * `sentMessageIds` and `deletedMessageIds` are used to show the notifications upon message deletion.
   * The corresponding messages are flagged respectively using the `isOwnMessage` and `wasDeletedByUser` booleans which are attached to `messages` (used for rendering).
   */
  const sentMessageIds = useRef([]);
  const deletedMessageIds = useRef([]);
  const { userData, isSessionValid } = useUser();
  const { username: ownUsername } = userData || {};
  const savedMessages = useRef({});
  const { channelData, refreshChannelData } = useChannel();
  const { username: chatRoomOwnerUsername, isViewerBanned = false } =
    channelData || {};
  const { notifyError, dismissNotif } = useNotif();
  const retryConnectionAttemptsCounterRef = useRef(0);
  const chatCapabilities = useRef([]);

  // Connection State
  const [hasConnectionError, setHasConnectionError] = useState();
  const [sendAttemptError, setSendAttemptError] = useState();
  const connection = useRef(null);
  const [room, setRoom] = useState(null);
  const isConnectionOpenRef = useRef(false);

  const isInitializingConnection = useRef(false);
  const abortControllerRef = useRef();
  const isConnecting = isInitializingConnection.current;

  // Chat Actions
  const [deletedMessage, setDeletedMessage] = useState();
  const { actions, chatUserRole, updateUserRole } = useChatActions({
    chatCapabilities,
    isConnectionOpen: isConnectionOpenRef.current,
    connection,
    setSendAttemptError
  });
  const isModerator = chatUserRole === CHAT_USER_ROLE.MODERATOR;

  // Poll Stream Action
  const {
    updatePollData,
    votes,
    hasPollEnded,
    resetPollProps,
    isActive,
    clearPollLocalStorage,
    setSelectedOption,
    selectedOption,
    showFinalResults,
    duration,
    question,
    expiry,
    startTime,
    noVotesCaptured,
    tieFound,
    savedPollData,
    saveVotesToLocalStorage,
    savePollDataToLocalStorage,
    dispatchPollState,
    endPollAndResetPollProps
  } = usePoll();
  const { pathname } = useLocation();

  const isStreamManagerPage = pathname === '/manager';

  const startPoll = useCallback(
    async (pollStreamActionData) => {
      const attributes = {
        eventType: START_POLL,
        pollStreamActionData: JSON.stringify(pollStreamActionData)
      };

      await actions.sendMessage(START_POLL, attributes);
      return true;
    },
    [actions]
  );

  const endPoll = useCallback(
    ({ withTimeout, timeoutDuration } = {}) => {
      const content = 'end poll';
      const attributes = { eventType: END_POLL };
      if (withTimeout) {
        setTimeout(
          () => actions.sendMessage(content, attributes),
          timeoutDuration
        );
      } else {
        actions.sendMessage(content, attributes);
      }
    },
    [actions]
  );

  const sendHeartBeat = useCallback(() => {
    if (
      isModerator &&
      isActive &&
      !showFinalResults &&
      !noVotesCaptured &&
      !tieFound
    ) {
      actions.sendMessage(HEART_BEAT, {
        eventType: HEART_BEAT,
        updatedVotes: JSON.stringify(votes),
        duration: JSON.stringify(duration),
        question: JSON.stringify(question),
        expiry: JSON.stringify(expiry),
        startTime: JSON.stringify(startTime),
        voters: JSON.stringify(savedPollData?.voters || {})
      });
    }
  }, [
    actions,
    duration,
    expiry,
    isActive,
    isModerator,
    noVotesCaptured,
    question,
    savedPollData?.voters,
    showFinalResults,
    startTime,
    tieFound,
    votes
  ]);

  useEffect(() => {
    let heartBeatIntervalId = null;
    if (isActive) {
      heartBeatIntervalId = setInterval(() => {
        sendHeartBeat();
      }, 4000);
    }

    return () => {
      if (heartBeatIntervalId !== null) {
        clearInterval(heartBeatIntervalId);
      }
    };
  }, [isActive, sendHeartBeat]);

  const initMessages = useCallback(() => {
    const initialMessages = savedMessages.current[chatRoomOwnerUsername] || [];

    dispatch({ type: actionTypes.INIT_MESSAGES, initialMessages });
  }, [chatRoomOwnerUsername]);

  const addMessage = useCallback(
    (message) => {
      const isOwnMessage = ownUsername === message.sender.userId;

      // Upon receiving a new message, we detect if the message was sent by the current user
      if (isOwnMessage) sentMessageIds.current.push(message.id);

      dispatch({ type: actionTypes.ADD_MESSAGE, message, isOwnMessage });
    },
    [ownUsername]
  );

  const removeMessage = useCallback((messageId) => {
    dispatch({
      type: actionTypes.DELETE_MESSAGE,
      messageId,
      deletedMessageIds
    });
  }, []);

  const removeMessageByUserId = useCallback((userId) => {
    dispatch({
      type: actionTypes.DELETE_MESSAGES_BY_USER_ID,
      userId
    });
  }, []);

  // messages local state
  const handleDeleteMessage = useCallback(
    (messageId) => {
      removeMessage(messageId);
      setDeletedMessage(messageId);
    },
    [removeMessage]
  );

  const handleUserDisconnect = useCallback(
    (bannedUsername) => {
      const bannedUserChannelId =
        extractChannelIdfromChannelArn(bannedUsername);

      if (bannedUserChannelId === userData?.trackingId.toLowerCase()) {
        // This user has been banned
        notifyError($content.notifications.error.you_have_been_banned);
        refreshChannelData();
      }
    },
    [notifyError, refreshChannelData, userData?.trackingId]
  );

  const disconnect = useCallback(() => {
    refreshChannelData();
    setRoom(null);
    connection.current = null;
    chatCapabilities.current = null;
    isInitializingConnection.current = false;
    isConnectionOpenRef.current = false;
  }, [refreshChannelData]);

  const connect = useCallback(() => {
    if (
      isViewerBanned !== false ||
      !chatRoomOwnerUsername ||
      isInitializingConnection.current
    )
      return;

    // Clean up previous connection resources
    abortControllerRef.current = new AbortController();
    if (connection.current) disconnect();

    isInitializingConnection.current = true;
    setHasConnectionError(false);

    // create a new instance of chat room
    const { signal } = abortControllerRef.current;
    const room = new ChatRoom({
      regionOrUrl: ivsChatWebSocketRegionOrUrl,
      maxReconnectAttempts: MAX_RECONNECT_ATTEMPTS,
      tokenProvider: async () => {
        const data = await requestChatToken(chatRoomOwnerUsername, signal);

        if (data?.error) {
          retryConnectionAttemptsCounterRef.current += 1;
          if (
            retryConnectionAttemptsCounterRef.current === MAX_RECONNECT_ATTEMPTS
          ) {
            isInitializingConnection.current = false;
            notifyError($content.notifications.error.error_loading_chat, {
              withTimeout: false
            });
            setHasConnectionError(true);
          }
        } else {
          chatCapabilities.current = data.capabilities;
        }

        return {
          ...data,
          ...(!data?.error && {
            sessionExpirationTime: new Date(data.sessionExpirationTime)
          })
        };
      }
    });

    room.logLevel = process.env.REACT_APP_STAGE === 'prod' ? info : debug;
    room.connect();
    setRoom(room);
    connection.current = room;
    isConnectionOpenRef.current = true;
    isInitializingConnection.current = false;
  }, [chatRoomOwnerUsername, disconnect, isViewerBanned, notifyError]);

  // Initialize connection
  useEffect(() => {
    connect();

    return disconnect;
  }, [connect, disconnect]);

  useEffect(() => {
    // If chat room listeners are not available, do not continue
    if (!room || !room.addListener) {
      return;
    }

    const unsubscribeOnConnect = room.addListener('connect', () => {
      updateUserRole();
      dismissNotif();
    });

    const unsubscribeOnDisconnect = room.addListener('disconnect', () => {
      isConnectionOpenRef.current = false;
      connection.current = null;
      setRoom(null);
      chatCapabilities.current = [];

      updateUserRole();
    });

    const unsubscribeOnUserDisconnect = room.addListener(
      'userDisconnect',
      (event) => {
        const { trackingId } = userData;
        const { userId: bannedUserId } = event;

        handleUserDisconnect(bannedUserId);

        const bannedUserChannelId =
          extractChannelIdfromChannelArn(bannedUserId);
        if (bannedUserChannelId !== trackingId.toLowerCase()) {
          removeMessageByUserId(bannedUserId);
        }
      }
    );

    const unsubscribeOnMessage = room.addListener('message', (message) => {
      const {
        attributes: {
          pollStreamActionData = undefined,
          eventType = undefined,
          voter = undefined,
          option = undefined
        }
      } = message;
      switch (eventType) {
        case HEART_BEAT:
          if ((isModerator && isStreamManagerPage) || hasPollEnded) return;

          const date = JSON.parse(message.attributes.startTime);
          const currentTime = Date.now();
          const delay = (currentTime - date) / 1000;

          updatePollData({
            duration: Number(JSON.parse(message.attributes.duration)),
            question: JSON.parse(message.attributes.question),
            votes: JSON.parse(message.attributes.updatedVotes),
            isActive: true,
            expiry: JSON.parse(message.attributes.expiry),
            startTime: JSON.parse(message.attributes.startTime),
            delay
          });

          const votersList = JSON.parse(message.attributes.voters);

          if (!selectedOption && userData?.trackingId in votersList) {
            const savedVote = votersList[userData?.trackingId];
            if (savedVote) {
              setSelectedOption(savedVote);
              dispatchPollState({ isVoting: false });
            }
          }
          break;
        case SEND_VOTE_STATS:
          const updatedVotes = JSON.parse(message.attributes.updatedVotes);
          updatePollData({ votes: updatedVotes });
          break;
        case SUBMIT_VOTE:
          const shouldBlockVote = isVotingBlocked(
            JSON.parse(message.attributes.duration),
            JSON.parse(message.attributes.startTime)
          );

          const canProcessVote =
            isModerator && pathname === '/manager' && !shouldBlockVote;

          if (canProcessVote) {
            const currentVotes = updateVotes(message, votes);
            updatePollData({ votes: currentVotes });

            saveVotesToLocalStorage(currentVotes, { [voter]: option });

            actions.sendMessage(SEND_VOTE_STATS, {
              eventType: SEND_VOTE_STATS,
              updatedVotes: JSON.stringify(currentVotes)
            });
          }
          break;
        case START_POLL:
          const {
            votes: options,
            duration,
            question,
            expiry,
            startTime,
            delay: del = 0
          } = JSON.parse(pollStreamActionData);

          if (isModerator && isStreamManagerPage) {
            savePollDataToLocalStorage({
              duration,
              expiry,
              startTime,
              question,
              votes: options,
              voters: {},
              isActive: true,
              name: STREAM_ACTION_NAME.POLL
            });
          }

          updatePollData({
            duration,
            question,
            votes: options,
            isActive: true,
            expiry,
            startTime,
            delay: del
          });
          break;
        case END_POLL:
          endPollAndResetPollProps();
          break;
        case SEND_MESSAGE:
          addMessage(message);
          break;
        default:
          break;
      }
    });

    const unsubscribeOnMessageDelete = room.addListener(
      'messageDelete',
      (deletedMessage) => {
        const {
          attributes: { MessageID },
          reason
        } = deletedMessage;
        handleDeleteMessage(MessageID, reason);
      }
    );

    return () => {
      unsubscribeOnConnect();
      unsubscribeOnDisconnect();
      unsubscribeOnMessage();
      unsubscribeOnMessageDelete();
      unsubscribeOnUserDisconnect();
    };
  }, [
    addMessage,
    room,
    updateUserRole,
    dismissNotif,
    handleDeleteMessage,
    handleUserDisconnect,
    userData,
    removeMessageByUserId,
    updatePollData,
    resetPollProps,
    hasPollEnded,
    isModerator,
    pathname,
    votes,
    actions,
    isActive,
    selectedOption,
    setSelectedOption,
    saveVotesToLocalStorage,
    savedPollData,
    clearPollLocalStorage,
    isStreamManagerPage,
    savePollDataToLocalStorage,
    dispatchPollState,
    endPollAndResetPollProps
  ]);

  // We are saving the chat messages in local state for only the currently signed-in user's chat room,
  // and removing them from local state once the user has signed out
  useEffect(() => {
    if (isSessionValid) {
      if (
        ownUsername &&
        chatRoomOwnerUsername &&
        chatRoomOwnerUsername === ownUsername
      ) {
        savedMessages.current[ownUsername] = messages.map((message) => ({
          ...message,
          isPreloaded: true
        }));
      }
    } else {
      savedMessages.current = {};
    }
  }, [isSessionValid, messages, ownUsername, chatRoomOwnerUsername]);

  const value = useMemo(
    () => ({
      addMessage,
      deletedMessageIds,
      initMessages,
      messages,
      removeMessage,
      removeMessageByUserId,
      sentMessageIds,
      // chat Actions
      actions,
      chatUserRole,
      hasConnectionError,
      isConnecting,
      sendAttemptError,
      isModerator,
      startPoll,
      endPoll,
      deletedMessage,
      setDeletedMessage
    }),
    [
      actions,
      addMessage,
      chatUserRole,
      hasConnectionError,
      initMessages,
      isConnecting,
      messages,
      removeMessage,
      removeMessageByUserId,
      sendAttemptError,
      isModerator,
      startPoll,
      endPoll,
      deletedMessage,
      setDeletedMessage
    ]
  );

  return <Context.Provider value={value}>{children}</Context.Provider>;
};

Provider.propTypes = { children: PropTypes.node.isRequired };

export const useChat = () => useContextHook(Context);
