import React, { useCallback, useEffect, useRef, useMemo, useState } from 'react'

import { ActionEmitter, KeybindAction } from '../keybindings'
import { marknoticedChat, saveLastChatId } from '../backend/chat'
import { BackendRemote } from '../backend-com'

import type { RefObject, PropsWithChildren } from 'react'
import type { T } from '@deltachat/jsonrpc-client'
import { useFetch } from '../hooks/useFetch'
import { getLogger } from '@deltachat-desktop/shared/logger'

const log = getLogger('ChatContext')

export type SelectChat = (nextAccountId: number, chatId: number) => void

export type UnselectChat = () => void

export type ChatContextValue = {
  /**
   * `withLinger` means that after `selectChat()` the value of `chatWithLinger`
   * does not change immediately (unlike `chatId`),
   * until the new chat's info gets loaded.
   */
  chatWithLinger?: T.FullChat
  chatNoLinger?: T.FullChat
  loadingChat: boolean
  chatId?: number
  /**
   * Changes the active chat.
   * Note that the chat may never get selected
   * if another call to `selecChat` interrupted it.
   *
   * @throws if `nextAccountId` is not the currently selected account.
   */
  selectChat: SelectChat
  unselectChat: UnselectChat
}

type Props = {
  accountId?: number
  /**
   * the ref gives us a handle to reset the component without moving it up in the hierarchy.
   * a class component would give us the option to call methods on the component,
   * but we are using a functional component here so we need to pass this as a property instead*/
  unselectChatRef: RefObject<UnselectChat | null>
}

export const ChatContext = React.createContext<ChatContextValue | null>(null)

/**
 * This context tries to be capable of handling `accountId` changing,
 * i.e. it does not need to be behind `key={accountId}`.
 */
export const ChatProvider = ({
  children,
  accountId,
  unselectChatRef,
}: PropsWithChildren<Props>) => {
  const sessionId = useMemo(
    () => Symbol(),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [accountId]
  )
  const sessionIdRef = useRef(sessionId)
  sessionIdRef.current = sessionId

  // When the `accountId` changes, we invalidate `chatId`,
  // and the `setChatId` function.
  //
  // We usually utilize `key={accountId}` for this,
  // but applying `key=` to a React context provider
  // makes _all_ its descendants (as opposed just the components
  // that depend on the context) re-render, including `AccountListSidebar`.
  const [chatId_, setChatId_] = useState<
    undefined | { sessionId: symbol; chatId: number }
  >(undefined)
  const chatId =
    chatId_ == undefined || chatId_.sessionId !== sessionId
      ? undefined
      : chatId_.chatId
  const setChatId = useCallback(
    (newChatId: number | undefined) => {
      if (sessionId !== sessionIdRef.current) {
        // This may happen when `selectChat` gets called
        // from `Promise.then` or `setTimeout` and such.
        log.info(
          "Called setChatId, but we've already switched the account after the closure was created, ignoring"
        )
        return
      }
      setChatId_(
        newChatId == undefined ? undefined : { chatId: newChatId, sessionId }
      )
    },
    [sessionId]
  )

  useEffect(() => {
    window.__selectedChatId = chatId
  }, [chatId])

  const chatFetch = useFetch(
    useCallback(
      async (accountId: number, chatId: number) => ({
        chat: await BackendRemote.rpc.getFullChatById(accountId, chatId),
        accountId,
      }),
      []
    ),
    accountId != undefined && chatId != undefined ? [accountId, chatId] : null
  )
  if (chatFetch?.result?.ok === false) {
    log.error('Failed to fetch chat', chatFetch.result.err)
  }
  const chatWithLinger =
    chatFetch?.lingeringResult?.ok &&
    // Make sure that the chat belongs to the current account,
    // to prevent data leaking between accounts, and weird race-y bugs.
    //
    // I think now that we have `sessionId` this check is no longer needed,
    // because that basically ensures that `chatId` is always valid
    // for the current `accountId`?
    accountId === chatFetch.lingeringResult.value.accountId
      ? chatFetch.lingeringResult.value.chat
      : undefined
  const chatNoLinger =
    chatFetch?.result?.ok &&
    // For non-lingering result we expect `accountId` to always be equal,
    // but let's double-check + to be consistent with the same check above.
    accountId === chatFetch.result.value.accountId
      ? chatFetch.result.value.chat
      : undefined

  const selectChat = useCallback<SelectChat>(
    (nextAccountId: number, nextChatId: number) => {
      if (!accountId) {
        throw new Error('can not select chat when no `accountId` is given')
      }

      if (accountId !== nextAccountId) {
        throw new Error(
          'accountid of ChatProvider context is not equal to nextAccountId'
        )
      }

      // Jump to last message if user clicked chat twice
      // Remember that there might be no messages in the chat.
      // @TODO: We probably want this to be part of the UI logic instead
      if (nextChatId === chatId) {
        const focusMessage = false
        window.__internal_jump_to_message_asap = {
          accountId,
          chatId: nextChatId,
          jumpToMessageArgs: [
            {
              msgId: undefined,
              highlight: false,
              focus: focusMessage,
              addMessageIdToStack: undefined,
              // `scrollIntoViewArg:` doesn't really have effect when
              // jumping to the last message.
            },
          ],
        }
        window.__internal_check_jump_to_message?.()

        // Yes, we only run this in the `nextChatId === chatId` case.
        // Otherwise the composer will get focused when the chat loads,
        // in another `useEffect`.
        if (!focusMessage) {
          const composerTextarea = document.getElementsByClassName(
            'create-or-edit-message-input'
          )[0]
          if (composerTextarea instanceof HTMLElement) {
            composerTextarea.focus()
          } else {
            log.warn(
              'Failed to focus composer: not an HTMLElement',
              composerTextarea
            )
          }
        }
      }

      // Already set known state
      setChatId(nextChatId)

      // Clear system notifications and mark chat as seen in backend
      marknoticedChat(accountId, nextChatId)

      // Remember that user selected this chat to open it again when they come back
      saveLastChatId(accountId, nextChatId)
    },
    [accountId, chatId, setChatId]
  )

  const unselectChat = useCallback<UnselectChat>(() => {
    setChatId(undefined)
  }, [setChatId])

  // Callback ref pattern: keeping ref in sync for external callers
  unselectChatRef.current = unselectChat

  const lastArchivedCheckChatId = useRef<number | undefined>(undefined)
  if (chatNoLinger != undefined && chatId !== lastArchivedCheckChatId.current) {
    lastArchivedCheckChatId.current = chatId
    // Switch to "archived" view if selected chat is there
    // @TODO: We probably want this to be part of the UI logic instead
    ActionEmitter.emitAction(
      chatNoLinger.archived
        ? KeybindAction.ChatList_SwitchToArchiveView
        : KeybindAction.ChatList_SwitchToNormalView
    )
  }

  const refreshChat = chatFetch?.refresh
  // Subscribe to events coming from the core
  useEffect(() => {
    const onChatModified = (
      eventAccountId: number,
      { chatId: eventChatId }: { chatId: number }
    ) => {
      if (eventAccountId !== accountId) {
        return
      }

      if (eventChatId !== chatId) {
        return
      }

      if (refreshChat) {
        refreshChat()
      } else {
        log.error("tried to refreshChat, but it's", refreshChat)
      }
    }

    const onContactsModified = (
      eventAccountId: number,
      { contactId }: { contactId: number | null }
    ) => {
      if (eventAccountId !== accountId) {
        return
      }

      if (!chatWithLinger) {
        return
      }

      if (!contactId) {
        return
      }

      if (!chatWithLinger.contactIds.includes(contactId)) {
        return
      }

      if (refreshChat) {
        refreshChat()
      } else {
        log.error("tried to refreshChat, but it's", refreshChat)
      }
    }

    BackendRemote.on('ChatModified', onChatModified)
    BackendRemote.on('ChatEphemeralTimerModified', onChatModified)
    BackendRemote.on('ContactsChanged', onContactsModified)

    return () => {
      BackendRemote.off('ChatModified', onChatModified)
      BackendRemote.off('ChatEphemeralTimerModified', onChatModified)
      BackendRemote.off('ContactsChanged', onContactsModified)
    }
  }, [accountId, chatWithLinger, chatId, refreshChat])

  const loadingChat = chatFetch?.loading ?? false
  const value: ChatContextValue = useMemo(
    () => ({
      chatWithLinger,
      chatNoLinger,
      loadingChat,
      chatId,
      selectChat,
      unselectChat,
    }),
    [
      chatWithLinger,
      chatNoLinger,
      loadingChat,
      chatId,
      selectChat,
      unselectChat,
    ]
  )

  return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>
}
