"use client"

import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react"
import { useSearchParams, useRouter } from "next/navigation"
import { useQueryClient } from "@tanstack/react-query"
import { buildLabelTextToNavColorClass } from "@/components/gmail/mail-label-pills"
import { useIsDemoMail } from "@/lib/demo/demo-mail-context"
import { useDemoMailStore } from "@/lib/demo/demo-mail-store"
import {
  useMessages,
  useMailSearch,
  fetchMessagesListPage,
  messagesListQueryKey,
} from "@/lib/api/hooks/use-mail-queries"
import {
  useUpdateFlags,
  useUpdateLabels,
  useDeleteMessage,
} from "@/lib/api/hooks/use-mail-mutations"
import type { ApiMessageSummary, PaginatedResponse } from "@/lib/api/types"
import type { Email, EmailAttachment } from "@/lib/email-data"
import {
  isThreadHeadMessage,
} from "@/lib/mail-thread"
import { repairSnippet } from "@/lib/mail-mime-body"
import { useScheduledMail } from "@/lib/scheduled-mail-context"
import { useMailStore } from "@/lib/stores/mail-store"
import { useScheduledStore } from "@/lib/stores/scheduled-store"
import { usePersistHydrated } from "@/hooks/use-persist-hydrated"
import { useIsMd } from "@/hooks/use-md-breakpoint"
import { sortEmailsForInbox } from "@/lib/mail-settings/sort-emails"
import { useMailSettingsStore } from "@/lib/stores/mail-settings-store"
import { useActiveAccount } from "@/lib/stores/account-store"
import { useMailSearchStore } from "@/lib/stores/mail-search-store"
import {
  emailMatchesInboxTab,
  type MailFolderFilterCtx,
  type MailNavFolderMaps,
} from "@/lib/mail-folder-filter"
import {
  getMailNavFolderLabel,
  inboxTabDisplayLabel,
} from "@/lib/sidebar-nav-data"
import { buildInboxCategoryTabIcons } from "@/lib/inbox-category-tabs"
import {
  INBOX_ALL_TAB,
  SEARCH_FOLDER_ID,
  inboxTabShowsInactiveMeta,
  normalizeInboxTabSegment,
} from "@/lib/mail-url"
import {
  parseSearchParams,
  type SearchParams,
} from "@/lib/mail-search/search-params"
import { useBuildSearchUrl } from "@/hooks/use-build-search-url"
import { searchParamsToMessageSearchFilter } from "@/lib/mail-search/search-filter"
import { useSidebarNav, registerNavEmailSync } from "@/lib/sidebar-nav-context"
import { useMoveTargets } from "@/components/gmail/move-to-menu-items"
import { buildListMailIndex } from "@/components/gmail/email-list/list-mail-index"
import {
  useComposeActions,
  useComposeDrafts,
} from "@/lib/compose-context"
import {
  type EmailListProps,
  buildInboxTabBarItems,
} from "@/components/gmail/email-list/email-list-helpers"
import { useMailListPullRefresh } from "@/hooks/use-mail-list-pull-refresh"
import { useMailListInfiniteScroll } from "@/hooks/use-mail-list-infinite-scroll"
import { ensureVcLogosCollection } from "@/lib/register-vc-logos"
import { resolveListRowAttachments } from "@/lib/attachment-display"
import { useListMessageAttachments } from "@/lib/api/hooks/use-list-message-attachments"
import { resolveParsedCalendarInvitation } from "@/lib/resolve-email-calendar-invitation"
import { resolveEmailInboxCategoryTabs } from "@/lib/inbox-category-tabs"
import { cleanSenderName } from "@/lib/sender-display"
import { threadStoreId } from "@/lib/mail-settings/list-row-id"
import { useIsXs } from "@/hooks/use-xs"
import { useTouchNav } from "@/hooks/use-touch-nav"
import type { MessageSearchFilter } from "@/lib/api/types"
import {
  mailFlagIsRead,
  mailFlagIsStarred,
  mailFlagIsImportant,
  mailFlagsWithRead,
  mailFlagsWithStarred,
  mailFlagsWithImportant,
} from "@/lib/mail-flags"
import { LIST_PAGE_SIZE, type ListPageSize } from "@/lib/mail-list-page-size"

function apiMessageToEmail(msg: ApiMessageSummary): Email {
  const sender = msg.from[0]?.name || msg.from[0]?.address || ""
  const senderEmail = msg.from[0]?.address || ""
  return {
    id: msg.id,
    sender,
    senderEmail,
    subject: msg.subject,
    preview: repairSnippet(msg.snippet) ?? msg.snippet,
    date: msg.date,
    read: mailFlagIsRead(msg.flags),
    starred: mailFlagIsStarred(msg.flags),
    important: mailFlagIsImportant(msg.flags, msg.labels),
    spam: msg.labels.includes("spam"),
    hasAttachment: msg.has_attachments,
    labels: msg.labels,
    threadHeadId: msg.thread_id ?? msg.id,
    threadMessageIds: [msg.id],
    isThreadHead: true,
  }
}

export function useEmailListData({
  selectedFolder,
  inboxTab,
  listPage,
  openMailId,
  splitView = false,
  onMailRouteNavigate,
  onFolderUnreadCountsChange,
}: EmailListProps) {
  const isViewMode = openMailId !== null && !splitView
  const showSplitReadingPane = splitView && openMailId !== null
  const isSearchMode = selectedFolder === SEARCH_FOLDER_ID
  const searchRouter = useRouter()
  const buildSearchUrl = useBuildSearchUrl()
  const searchAccount = useActiveAccount()
  const setAdvancedOpen = useMailSearchStore((s) => s.setAdvancedOpen)
  const urlSearchParams = useSearchParams()
  const searchParams = useMemo(
    () => (isSearchMode ? parseSearchParams(urlSearchParams) : null),
    [isSearchMode, urlSearchParams]
  )

  const setSearchFilter = useCallback(
    (patch: Partial<SearchParams>) => {
      if (!searchParams) return
      searchRouter.push(buildSearchUrl({ ...searchParams, ...patch }))
    },
    [searchParams, searchRouter, buildSearchUrl]
  )

  const toggleSearchFilter = useCallback(
    (key: keyof SearchParams, value: string) => {
      if (!searchParams) return
      const next = { ...searchParams }
      if (key === "has") {
        const arr = [...next.has]
        if (arr.includes(value)) next.has = arr.filter((v) => v !== value)
        else next.has = [...arr, value]
      } else if (key === "excludeChats") {
        next.excludeChats = !next.excludeChats
      } else {
        const cur = (next as Record<string, unknown>)[key]
        ;(next as Record<string, unknown>)[key] = cur === value ? "" : value
      }
      searchRouter.push(buildSearchUrl(next))
    },
    [searchParams, searchRouter, buildSearchUrl]
  )

  const { savedThreadReplyDrafts } = useComposeDrafts()
  const {
    openCompose,
    openComposeWithInitial,
    closeAllInlineComposes,
    pruneInlineComposesToOpenThread,
  } = useComposeActions()

  const {
    scheduledEmails,
    snoozedEmails,
    requestDeleteScheduled,
    requestArchiveScheduled,
    requestSnoozeScheduled,
    requestToggleReadScheduled,
    requestRescheduleScheduled,
    requestGetScheduledEditPayload,
    requestSendScheduledNow,
    requestSnoozeMailboxEmail,
    requestRestoreSnoozedToInbox,
  } = useScheduledMail()

  const scheduledPersistHydrated = usePersistHydrated(useScheduledStore)
  const isDemoMail = useIsDemoMail()
  const demoVersion = useDemoMailStore((s) => s.version)

  const accountId = searchAccount?.id
  const queryClient = useQueryClient()
  const listPageSize = useMailSettingsStore((s) => s.listPageSize)
  const setListPageSize = useMailSettingsStore((s) => s.setListPageSize)
  const infiniteScroll = useMailSettingsStore((s) => s.infiniteScroll)
  const isXs = useIsXs()
  const touchNav = useTouchNav()

  const effectiveApiFolder = useMemo(() => {
    if (isSearchMode) return "__search__"
    if (selectedFolder === "scheduled" || selectedFolder === "snoozed") return "__local__"
    if (selectedFolder === "inbox") return "inbox"
    return selectedFolder
  }, [selectedFolder, isSearchMode])

  const searchFilter = useMemo<MessageSearchFilter | null>(() => {
    if (!isSearchMode || !searchParams) return null
    return searchParamsToMessageSearchFilter(searchParams, accountId)
  }, [isSearchMode, searchParams, accountId])

  const scrollInfiniteList = (isXs || infiniteScroll) && !isViewMode
  const usesApiInfiniteScroll =
    scrollInfiniteList &&
    effectiveApiFolder !== "__local__" &&
    !isSearchMode
  const messagesApiFolder =
    effectiveApiFolder === "__search__" || effectiveApiFolder === "__local__"
      ? "inbox"
      : effectiveApiFolder
  const messagesQueryContextKey = useMemo(
    () =>
      `${messagesApiFolder}:${accountId ?? ""}:${listPageSize}:${isDemoMail ? `demo-${demoVersion}` : "live"}`,
    [messagesApiFolder, accountId, listPageSize, isDemoMail, demoVersion]
  )
  const messagesQueryPage = usesApiInfiniteScroll ? 1 : listPage

  const messagesQuery = useMessages(
    messagesApiFolder,
    accountId,
    messagesQueryPage,
    listPageSize
  )

  const searchQuery = useMailSearch(searchFilter)

  const updateFlags = useUpdateFlags()
  const updateLabels = useUpdateLabels()
  const deleteMessage = useDeleteMessage()

  const apiMessages: ApiMessageSummary[] = useMemo(() => {
    if (isSearchMode) return searchQuery.data?.data ?? []
    if (effectiveApiFolder === "__local__") return []
    return messagesQuery.data?.data ?? []
  }, [isSearchMode, effectiveApiFolder, searchQuery.data, messagesQuery.data])

  const apiEmails: Email[] = useMemo(
    () => apiMessages.map(apiMessageToEmail),
    [apiMessages]
  )

  const apiMessagesById = useMemo(
    () => new Map(apiMessages.map((m) => [m.id, m])),
    [apiMessages]
  )

  const allEmails = useMemo(() => {
    if (selectedFolder === "scheduled" && scheduledPersistHydrated) {
      return scheduledEmails.map<Email>((entry) => ({
        id: entry.id,
        sender: entry.to[0]?.name ?? "Destinataire",
        senderEmail: entry.to[0]?.address,
        subject: entry.subject || "(Sans objet)",
        preview: "",
        body: "",
        date: entry.scheduled_at ?? entry.created_at,
        read: true,
        starred: false,
        important: false,
        labels: ["scheduled"],
        scheduledSendAt: entry.scheduled_at,
        scheduledToName: entry.to[0]?.name,
      }))
    }
    if (selectedFolder === "snoozed" && scheduledPersistHydrated) {
      return snoozedEmails
    }
    return apiEmails
  }, [
    selectedFolder,
    scheduledPersistHydrated,
    scheduledEmails,
    snoozedEmails,
    apiEmails,
  ])

  const emailById = useMemo(
    () => new Map(allEmails.map((e) => [e.id, e])),
    [allEmails]
  )

  const isLoading = isSearchMode ? searchQuery.isLoading : messagesQuery.isLoading
  const error = isSearchMode ? searchQuery.error : messagesQuery.error
  const isFetching = isSearchMode ? searchQuery.isFetching : messagesQuery.isFetching

  const sidebarNav = useSidebarNav()
  const navMaps = useMemo<MailNavFolderMaps>(
    () => ({
      folderIdToLabel: sidebarNav.folderIdToLabel,
      folderTree: sidebarNav.folderTree,
      labelRows: sidebarNav.labelRows,
    }),
    [sidebarNav.folderIdToLabel, sidebarNav.folderTree, sidebarNav.labelRows]
  )

  const inboxCategoryTabIconsCatalog = useMemo(
    () => buildInboxCategoryTabIcons(sidebarNav.labelRows),
    [sidebarNav.labelRows]
  )

  const inboxTabBarItems = useMemo(
    () => buildInboxTabBarItems(sidebarNav.labelRows),
    [sidebarNav.labelRows]
  )

  const listRowLabelBgByTextLower = useMemo(
    () => buildLabelTextToNavColorClass(sidebarNav.folderTree, sidebarNav.labelRows),
    [sidebarNav.folderTree, sidebarNav.labelRows]
  )

  const [rescheduleTarget, setRescheduleTarget] = useState<{
    id: string
    value: string
    panelOpen: boolean
  } | null>(null)
  const rescheduleDismissTimeoutsRef = useRef<
    Map<string, ReturnType<typeof setTimeout>>
  >(new Map())

  const scheduleReschedulePopoverDismiss = useCallback((rowId: string) => {
    const existing = rescheduleDismissTimeoutsRef.current.get(rowId)
    if (existing) clearTimeout(existing)
    const t = setTimeout(() => {
      rescheduleDismissTimeoutsRef.current.delete(rowId)
      setRescheduleTarget((p) => (p?.id === rowId ? null : p))
    }, 280)
    rescheduleDismissTimeoutsRef.current.set(rowId, t)
  }, [])

  useEffect(() => {
    const m = rescheduleDismissTimeoutsRef.current
    return () => {
      for (const t of m.values()) clearTimeout(t)
      m.clear()
    }
  }, [])

  useEffect(() => {
    ensureVcLogosCollection()
  }, [])

  const [cmScheduledRescheduleValue, setCmScheduledRescheduleValue] =
    useState("")

  const handleEditScheduledMail = useCallback(
    async (id: string) => {
      const payload = await requestGetScheduledEditPayload(id)
      if (!payload) return
      openComposeWithInitial({
        to: payload.to,
        subject: payload.subject,
        bodyHtml: payload.bodyHtml,
        editingScheduledId: id,
        scheduledSendAtIso: payload.sendAtIso,
        focusToOnMount: false,
        focusBodyOnMount: true,
      })
    },
    [requestGetScheduledEditPayload, openComposeWithInitial]
  )

  useEffect(() => {
    if (!openMailId) {
      closeAllInlineComposes()
    } else {
      const msg = emailById.get(openMailId)
      pruneInlineComposesToOpenThread(msg ? threadStoreId(msg) : openMailId)
    }
  }, [
    openMailId,
    emailById,
    closeAllInlineComposes,
    pruneInlineComposesToOpenThread,
  ])

  const conversationMode = useMailSettingsStore((s) => s.conversationMode)
  const inboxSort = useMailSettingsStore((s) => s.inboxSort)
  const density = useMailSettingsStore((s) => s.density)
  const isMd = useIsMd()

  const readOverrides = useMemo<Record<string, boolean>>(() => ({}), [])
  const starredEmails = useMemo<string[]>(() => [], [])
  const importantEmails = useMemo<string[]>(() => [], [])
  const labelEdits = useMemo(() => ({ additions: {} as Record<string, string[]>, removals: {} as Record<string, string[]> }), [])
  const hiddenEmailIds = useMemo<string[]>(() => [], [])
  const notSpamEmailIds = useMemo<string[]>(() => [], [])

  const setReadOverrides = useCallback(
    (updater: (prev: Record<string, boolean>) => Record<string, boolean>) => {
      const changes = updater({})
      for (const [id, isRead] of Object.entries(changes)) {
        const msg = apiMessagesById.get(id)
        if (!msg) continue
        const alreadyRead = mailFlagIsRead(msg.flags)
        if (isRead === alreadyRead) continue
        const flags = mailFlagsWithRead(msg.flags, isRead)
        updateFlags.mutate({ id, flags })
      }
    },
    [apiMessagesById, updateFlags]
  )

  const setLabelEdits = useCallback(
    (updater: (prev: { additions: Record<string, string[]>; removals: Record<string, string[]> }) => { additions: Record<string, string[]>; removals: Record<string, string[]> }) => {
      const result = updater({ additions: {}, removals: {} })
      for (const [id, additions] of Object.entries(result.additions)) {
        const msg = apiMessagesById.get(id)
        if (!msg) continue
        const newLabels = [...new Set([...msg.labels, ...additions])]
        const removals = result.removals[id] ?? []
        const finalLabels = newLabels.filter(
          (l) => !removals.some((r) => r.toLowerCase() === l.toLowerCase())
        )
        updateLabels.mutate({ id, labels: finalLabels })
      }
      for (const [id, removals] of Object.entries(result.removals)) {
        if (result.additions[id]) continue
        const msg = apiMessagesById.get(id)
        if (!msg) continue
        const finalLabels = msg.labels.filter(
          (l) => !removals.some((r) => r.toLowerCase() === l.toLowerCase())
        )
        updateLabels.mutate({ id, labels: finalLabels })
      }
    },
    [apiMessagesById, updateLabels]
  )

  const mailActions = useMemo(() => ({
    markSeen: (id: string) => useMailStore.getState().markSeen(id),
    pushRecentMoveTarget: (targetId: string) => useMailStore.getState().pushRecentMoveTarget(targetId),
    hideEmail: (id: string) => deleteMessage.mutate({ id }),
    hideEmails: (ids: string[]) => { for (const id of ids) deleteMessage.mutate({ id }) },
    markNotSpam: (id: string) => {
      const msg = apiMessagesById.get(id)
      if (!msg) return
      const newLabels = msg.labels.filter((l) => l !== "spam")
      if (!newLabels.includes("inbox")) newLabels.push("inbox")
      updateLabels.mutate({ id, labels: newLabels })
    },
    unhideEmail: (_id: string) => { /* no-op - API manages visibility */ },
    toggleStar: (id: string) => {
      const msg = apiMessagesById.get(id)
      if (!msg) return
      const starred = mailFlagIsStarred(msg.flags)
      updateFlags.mutate({ id, flags: mailFlagsWithStarred(msg.flags, !starred) })
    },
    toggleImportant: (id: string) => {
      const msg = apiMessagesById.get(id)
      if (!msg) return
      const important = mailFlagIsImportant(msg.flags, msg.labels)
      updateFlags.mutate({ id, flags: mailFlagsWithImportant(msg.flags, !important) })
    },
  }), [deleteMessage, updateLabels, updateFlags, apiMessagesById])

  useEffect(() => {
    registerNavEmailSync({
      renameLabel: (_from, _to) => {
        queryClient.invalidateQueries({ queryKey: ["messages"] })
      },
      removeLabel: (_label) => {
        queryClient.invalidateQueries({ queryKey: ["messages"] })
      },
    })
    return () => registerNavEmailSync(null)
  }, [queryClient])

  const [labelPickerQuery, setLabelPickerQuery] = useState("")
  const recentMoveTargets = useMailStore((s) => s.recentMoveTargets)
  const [mobileVisibleCount, setMobileVisibleCount] = useState(LIST_PAGE_SIZE)
  const [accumulatedApiEmails, setAccumulatedApiEmails] = useState<Email[]>([])
  const [loadedApiPage, setLoadedApiPage] = useState(1)
  const [isFetchingNextInfinitePage, setIsFetchingNextInfinitePage] =
    useState(false)
  const loadMoreSentinelRef = useRef<HTMLDivElement>(null)
  const infiniteListContextRef = useRef("")

  const seenEmailIdsRaw = useMailStore((s) => s.seenEmailIds)
  const seenEmailIds = useMemo(() => new Set(seenEmailIdsRaw), [seenEmailIdsRaw])

  const handleRefreshMessages = useCallback(async () => {
    await queryClient.invalidateQueries({ queryKey: ["messages"] })
  }, [queryClient])

  const {
    isRefreshing,
    setIsRefreshing,
    listViewportRef,
    pullContentRef,
    pullIconRef,
  } = useMailListPullRefresh({
    enabled: isXs && !isViewMode,
    isViewMode,
    onRefresh: handleRefreshMessages,
  })

  const handleManualRefresh = useCallback(async () => {
    if (isRefreshing) return
    setIsRefreshing(true)
    try {
      await handleRefreshMessages()
    } finally {
      setIsRefreshing(false)
    }
  }, [isRefreshing, handleRefreshMessages, setIsRefreshing])

  const markEmailSeen = useCallback((id: string) => {
    useMailStore.getState().markSeen(id)
  }, [])

  const folderFilterCtx = useMemo<MailFolderFilterCtx>(
    () => ({
      starredEmailIds: starredEmails,
      importantEmailIds: importantEmails,
    }),
    [starredEmails, importantEmails]
  )

  const filteredEmails = useMemo(() => {
    if (selectedFolder !== "inbox") return allEmails
    return allEmails.filter((e) =>
      emailMatchesInboxTab(e, inboxTab, folderFilterCtx, navMaps)
    )
  }, [allEmails, selectedFolder, inboxTab, folderFilterCtx, navMaps])

  const displayListEmails = useMemo(() => {
    let rows = filteredEmails
    if (conversationMode) {
      rows = rows.filter(isThreadHeadMessage)
    }
    return sortEmailsForInbox(
      rows,
      inboxSort,
      {
        readOverrides: {},
        starredIds: [],
        importantIds: [],
      },
      { conversationMode, byId: emailById }
    )
  }, [
    filteredEmails,
    conversationMode,
    inboxSort,
    emailById,
  ])

  const inboxCategoryTabLabel = useMemo(
    () =>
      inboxTabDisplayLabel(
        inboxTab,
        sidebarNav.labelRows,
        sidebarNav.folderIdToLabel
      ),
    [inboxTab, sidebarNav.labelRows, sidebarNav.folderIdToLabel]
  )

  const mobileUnreadCount = useMemo(
    () => displayListEmails.filter((e) => !e.read).length,
    [displayListEmails]
  )

  const mobileFolderLabel = useMemo(() => {
    if (isSearchMode) return "Résultats de recherche"
    const inboxTabNorm = normalizeInboxTabSegment(inboxTab)
    return selectedFolder === "inbox" && inboxTabNorm !== "primary"
      ? inboxCategoryTabLabel
      : getMailNavFolderLabel(selectedFolder, sidebarNav.folderIdToLabel)
  }, [
    selectedFolder,
    inboxTab,
    inboxCategoryTabLabel,
    sidebarNav.folderIdToLabel,
    isSearchMode,
  ])

  const paginationTotal = useMemo(() => {
    if (isSearchMode) return searchQuery.data?.pagination?.total
    if (effectiveApiFolder === "__local__") return allEmails.length
    return messagesQuery.data?.pagination?.total
  }, [isSearchMode, effectiveApiFolder, searchQuery.data, messagesQuery.data, allEmails.length])

  const totalPages = useMemo(
    () =>
      Math.max(
        1,
        Math.ceil((paginationTotal ?? displayListEmails.length) / listPageSize)
      ),
    [paginationTotal, displayListEmails.length, listPageSize]
  )

  const paginationRangeStart = useMemo(() => {
    if (displayListEmails.length === 0) return 0
    return (listPage - 1) * listPageSize + 1
  }, [displayListEmails.length, listPage, listPageSize])

  const paginationRangeEnd = useMemo(() => {
    if (displayListEmails.length === 0) return 0
    if (effectiveApiFolder !== "__local__" && !isSearchMode) {
      const total = paginationTotal ?? displayListEmails.length
      return Math.min(listPage * listPageSize, total)
    }
    return Math.min(listPage * listPageSize, displayListEmails.length)
  }, [
    displayListEmails.length,
    listPage,
    listPageSize,
    paginationTotal,
    effectiveApiFolder,
    isSearchMode,
  ])

  const handleListPageSizeChange = useCallback(
    (size: ListPageSize) => {
      if (size === listPageSize) return
      setListPageSize(size)
      onMailRouteNavigate({ page: 1 })
    },
    [listPageSize, setListPageSize, onMailRouteNavigate]
  )

  const processEmailsForDisplay = useCallback(
    (emails: Email[]) => {
      let rows =
        selectedFolder !== "inbox"
          ? emails
          : emails.filter((e) =>
              emailMatchesInboxTab(e, inboxTab, folderFilterCtx, navMaps)
            )
      if (conversationMode) {
        rows = rows.filter(isThreadHeadMessage)
      }
      const byId = new Map(emails.map((e) => [e.id, e]))
      return sortEmailsForInbox(
        rows,
        inboxSort,
        {
          readOverrides: {},
          starredIds: [],
          importantIds: [],
        },
        { conversationMode, byId }
      )
    },
    [
      selectedFolder,
      inboxTab,
      folderFilterCtx,
      navMaps,
      conversationMode,
      inboxSort,
    ]
  )

  const infiniteListContextKey = `${selectedFolder}:${inboxTab}:${accountId ?? ""}:${messagesApiFolder}`

  useEffect(() => {
    if (!usesApiInfiniteScroll) {
      setAccumulatedApiEmails([])
      setLoadedApiPage(1)
      infiniteListContextRef.current = ""
      return
    }

    if (infiniteListContextRef.current !== infiniteListContextKey) {
      infiniteListContextRef.current = infiniteListContextKey
      setAccumulatedApiEmails(displayListEmails)
      setLoadedApiPage(1)
      setMobileVisibleCount(LIST_PAGE_SIZE)
      listViewportRef.current?.scrollTo(0, 0)
      return
    }

    if (loadedApiPage === 1) {
      setAccumulatedApiEmails(displayListEmails)
    }
  }, [
    usesApiInfiniteScroll,
    infiniteListContextKey,
    displayListEmails,
    loadedApiPage,
    listViewportRef,
  ])

  const fetchNextApiPage = useCallback(async () => {
    if (!usesApiInfiniteScroll || isFetchingNextInfinitePage) return
    if (loadedApiPage >= totalPages) return

    const nextPage = loadedApiPage + 1
    setIsFetchingNextInfinitePage(true)
    try {
      const result = await queryClient.fetchQuery({
        queryKey: messagesListQueryKey(
          messagesApiFolder,
          accountId,
          nextPage,
          listPageSize,
          isDemoMail,
          demoVersion
        ),
        queryFn: () =>
          fetchMessagesListPage(
            messagesApiFolder,
            accountId,
            nextPage,
            listPageSize,
            isDemoMail
          ),
        staleTime: 60_000,
      })
      const processed = processEmailsForDisplay(
        result.data.map(apiMessageToEmail)
      )
      setAccumulatedApiEmails((prev) => {
        const ids = new Set(prev.map((e) => e.id))
        const appended = processed.filter((e) => !ids.has(e.id))
        if (appended.length === 0) return prev
        return [...prev, ...appended]
      })
      setLoadedApiPage(nextPage)

      if (nextPage < totalPages) {
        void queryClient.prefetchQuery({
          queryKey: messagesListQueryKey(
            messagesApiFolder,
            accountId,
            nextPage + 1,
            listPageSize,
            isDemoMail,
            demoVersion
          ),
          queryFn: () =>
            fetchMessagesListPage(
              messagesApiFolder,
              accountId,
              nextPage + 1,
              listPageSize,
              isDemoMail
            ),
          staleTime: 60_000,
        })
      }
    } finally {
      setIsFetchingNextInfinitePage(false)
    }
  }, [
    usesApiInfiniteScroll,
    isFetchingNextInfinitePage,
    loadedApiPage,
    totalPages,
    queryClient,
    messagesQueryContextKey,
    processEmailsForDisplay,
  ])

  useEffect(() => {
    if (!usesApiInfiniteScroll || loadedApiPage !== 1 || totalPages <= 1) return
    void queryClient.prefetchQuery({
      queryKey: messagesListQueryKey(
        messagesApiFolder,
        accountId,
        2,
        listPageSize,
        isDemoMail,
        demoVersion
      ),
      queryFn: () =>
        fetchMessagesListPage(
          messagesApiFolder,
          accountId,
          2,
          listPageSize,
          isDemoMail
        ),
      staleTime: 60_000,
    })
  }, [
    usesApiInfiniteScroll,
    loadedApiPage,
    totalPages,
    queryClient,
    messagesQueryContextKey,
  ])

  const infiniteScrollSourceEmails = usesApiInfiniteScroll
    ? accumulatedApiEmails
    : displayListEmails

  const pagedEmails = useMemo(() => {
    if (effectiveApiFolder !== "__local__" && !isSearchMode) {
      return displayListEmails
    }
    const start = (listPage - 1) * listPageSize
    return displayListEmails.slice(start, start + listPageSize)
  }, [displayListEmails, listPage, effectiveApiFolder, isSearchMode, listPageSize])

  const listEmails = useMemo(() => {
    if (!scrollInfiniteList) return pagedEmails
    if (usesApiInfiniteScroll) {
      if (isXs) {
        return infiniteScrollSourceEmails.slice(0, mobileVisibleCount)
      }
      return infiniteScrollSourceEmails
    }
    return displayListEmails.slice(0, mobileVisibleCount)
  }, [
    scrollInfiniteList,
    usesApiInfiniteScroll,
    isXs,
    infiniteScrollSourceEmails,
    mobileVisibleCount,
    displayListEmails,
    pagedEmails,
  ])

  const hasMoreInfinite = scrollInfiniteList
    ? usesApiInfiniteScroll
      ? isXs
        ? mobileVisibleCount < infiniteScrollSourceEmails.length ||
          loadedApiPage < totalPages
        : loadedApiPage < totalPages
      : mobileVisibleCount < displayListEmails.length
    : false

  const mobileVisibleCountRef = useRef(mobileVisibleCount)
  mobileVisibleCountRef.current = mobileVisibleCount

  const loadMoreInfinite = useCallback(() => {
    if (!scrollInfiniteList) return

    if (usesApiInfiniteScroll) {
      const sourceLength = infiniteScrollSourceEmails.length
      if (
        isXs &&
        mobileVisibleCountRef.current < sourceLength
      ) {
        setMobileVisibleCount((prev) =>
          Math.min(prev + LIST_PAGE_SIZE, sourceLength)
        )
        return
      }
      void fetchNextApiPage()
      return
    }

    setMobileVisibleCount((prev) =>
      Math.min(prev + LIST_PAGE_SIZE, displayListEmails.length)
    )
  }, [
    scrollInfiniteList,
    usesApiInfiniteScroll,
    isXs,
    infiniteScrollSourceEmails.length,
    fetchNextApiPage,
    displayListEmails.length,
  ])

  useMailListInfiniteScroll({
    enabled: scrollInfiniteList,
    sentinelRef: loadMoreSentinelRef,
    scrollRootRef: listViewportRef,
    hasMore: hasMoreInfinite,
    isLoadingMore: isFetchingNextInfinitePage,
    onLoadMore: loadMoreInfinite,
  })

  const listMailIndex = useMemo(() => buildListMailIndex(allEmails), [allEmails])

  const attachmentFetchIds = useMemo(
    () =>
      listEmails
        .filter((e) => e.hasAttachment && !(e.attachments?.length))
        .map((e) => e.id),
    [listEmails]
  )
  const { byId: fetchedAttachmentsById, stateById: attachmentFetchStateById } =
    useListMessageAttachments(attachmentFetchIds)

  const listRowExtras = useMemo(() => {
    const invitationById = new Map<
      string,
      ReturnType<typeof resolveParsedCalendarInvitation>
    >()
    const attachmentsById = new Map<string, EmailAttachment[]>()
    const categoryTabsById = new Map<
      string,
      ReturnType<typeof resolveEmailInboxCategoryTabs>
    >()
    const subtreeIdsCache = new Map<string, string[] | null>()
    const showCategoryTabIcons =
      selectedFolder === "inbox" &&
      normalizeInboxTabSegment(inboxTab) === INBOX_ALL_TAB

    for (const e of listEmails) {
      invitationById.set(e.id, resolveParsedCalendarInvitation(e))
      const fetchState = attachmentFetchStateById.get(e.id) ?? "idle"
      attachmentsById.set(
        e.id,
        resolveListRowAttachments(
          e,
          fetchedAttachmentsById.get(e.id),
          fetchState
        )
      )
      if (showCategoryTabIcons) {
        const tabs = resolveEmailInboxCategoryTabs(
          e,
          folderFilterCtx,
          navMaps,
          inboxCategoryTabIconsCatalog,
          subtreeIdsCache
        )
        if (tabs.length > 0) categoryTabsById.set(e.id, tabs)
      }
    }
    return { invitationById, attachmentsById, categoryTabsById }
  }, [
    listEmails,
    fetchedAttachmentsById,
    attachmentFetchStateById,
    selectedFolder,
    inboxTab,
    folderFilterCtx,
    navMaps,
    inboxCategoryTabIconsCatalog,
  ])

  const prevInfiniteScrollRef = useRef(infiniteScroll)
  useEffect(() => {
    const turnedOn = infiniteScroll && !prevInfiniteScrollRef.current
    prevInfiniteScrollRef.current = infiniteScroll
    if (!turnedOn || isXs) return
    setAccumulatedApiEmails([])
    setLoadedApiPage(1)
    setMobileVisibleCount(LIST_PAGE_SIZE)
    if (listPage !== 1) onMailRouteNavigate({ page: 1 })
  }, [infiniteScroll, isXs, listPage, onMailRouteNavigate])

  useEffect(() => {
    if (isXs || infiniteScroll) return
    if (listPage > totalPages) {
      onMailRouteNavigate({ page: totalPages })
    }
  }, [isXs, infiniteScroll, listPage, totalPages, onMailRouteNavigate])

  useEffect(() => {
    if (scrollInfiniteList) return
    listViewportRef.current?.scrollTo(0, 0)
  }, [listPage, selectedFolder, inboxTab, scrollInfiniteList, listViewportRef])

  useEffect(() => {
    if (!scrollInfiniteList || usesApiInfiniteScroll) return
    setMobileVisibleCount(LIST_PAGE_SIZE)
    listViewportRef.current?.scrollTo(0, 0)
  }, [selectedFolder, inboxTab, scrollInfiniteList, usesApiInfiniteScroll, listViewportRef])

  const moveTargets = useMoveTargets({
    folderTree: sidebarNav.folderTree,
    recentMoveTargets,
    currentFolderId: selectedFolder,
  })

  const folderUnreadCounts = useMemo<Record<string, number>>(() => ({}), [])

  const seenSerialized = useMemo(
    () => [...seenEmailIds].sort().join(","),
    [seenEmailIds]
  )

  const { unseenInTabById, tabUnseenSenderLineById } = useMemo(() => {
    const seen = new Set(
      seenSerialized.length > 0 ? seenSerialized.split(",") : []
    )
    const subtreeIdsCache = new Map<string, string[] | null>()
    const counts: Record<string, number> = {}
    const preview: Record<string, string> = {}
    for (const tab of inboxTabBarItems) {
      const rows: Email[] = []
      for (const e of allEmails) {
        if (seen.has(e.id)) continue
        if (
          !emailMatchesInboxTab(
            e,
            tab.id,
            folderFilterCtx,
            navMaps,
            subtreeIdsCache
          )
        ) {
          continue
        }
        rows.push(e)
      }
      counts[tab.id] = rows.length
      if (inboxTabShowsInactiveMeta(tab.id)) {
        const chain: string[] = []
        const used = new Set<string>()
        for (const e of rows) {
          const n = cleanSenderName(e.sender).trim()
          if (!n || used.has(n)) continue
          used.add(n)
          chain.push(n)
          if (chain.length >= 6) break
        }
        preview[tab.id] = chain.join(", ")
      }
    }
    return { unseenInTabById: counts, tabUnseenSenderLineById: preview }
  }, [
    seenSerialized,
    allEmails,
    inboxTabBarItems,
    folderFilterCtx,
    navMaps,
  ])

  useEffect(() => {
    onFolderUnreadCountsChange?.(folderUnreadCounts)
  }, [folderUnreadCounts, onFolderUnreadCountsChange])

  const listToolbarMode = splitView || !isViewMode
  const compactInboxTabs = isXs || splitView
  const activeInboxTabId = useMemo(
    () => normalizeInboxTabSegment(inboxTab),
    [inboxTab]
  )

  const pageIds = useMemo(() => listEmails.map((e) => e.id), [listEmails])
  const listRowsDep = listEmails.map((e) => e.id).join(",")

  const effectiveRead = useCallback(
    (email: Email) => email.read,
    []
  )

  const effectiveStarred = useCallback(
    (email: Email) => email.starred,
    []
  )

  const markAllInViewAsRead = useCallback(() => {
    for (const e of displayListEmails) {
      if (e.read) continue
      const msg = apiMessagesById.get(e.id)
      if (!msg) continue
      if (!mailFlagIsRead(msg.flags)) {
        updateFlags.mutate({ id: e.id, flags: mailFlagsWithRead(msg.flags, true) })
      }
    }
  }, [displayListEmails, apiMessagesById, updateFlags])

  return {
    selectedFolder,
    inboxTab,
    listPage,
    openMailId,
    splitView,
    isViewMode,
    showSplitReadingPane,
    isSearchMode,
    searchRouter,
    searchAccount,
    setAdvancedOpen,
    searchParams,
    setSearchFilter,
    toggleSearchFilter,
    savedThreadReplyDrafts,
    openCompose,
    openComposeWithInitial,
    allEmails,
    emailById,
    sidebarNav,
    navMaps,
    inboxCategoryTabIconsCatalog,
    inboxTabBarItems,
    listRowLabelBgByTextLower,
    rescheduleTarget,
    setRescheduleTarget,
    rescheduleDismissTimeoutsRef,
    scheduleReschedulePopoverDismiss,
    cmScheduledRescheduleValue,
    setCmScheduledRescheduleValue,
    handleEditScheduledMail,
    starredEmails,
    importantEmails,
    readOverrides,
    conversationMode,
    inboxSort,
    density,
    isMd,
    labelEdits,
    mailActions,
    setReadOverrides,
    setLabelEdits,
    labelPickerQuery,
    setLabelPickerQuery,
    hiddenEmailIds,
    notSpamEmailIds,
    recentMoveTargets,
    mobileVisibleCount,
    infiniteScroll,
    scrollInfiniteList,
    hasMoreInfinite,
    loadMoreSentinelRef,
    isFetchingNextInfinitePage,
    isXs,
    touchNav,
    seenEmailIds,
    isRefreshing,
    listViewportRef,
    pullContentRef,
    pullIconRef,
    handleManualRefresh,
    markEmailSeen,
    folderFilterCtx,
    filteredEmails,
    displayListEmails,
    inboxCategoryTabLabel,
    mobileUnreadCount,
    mobileFolderLabel,
    paginationTotal,
    listPageSize,
    paginationRangeStart,
    paginationRangeEnd,
    handleListPageSizeChange,
    totalPages,
    pagedEmails,
    listEmails,
    listMailIndex,
    listRowExtras,
    moveTargets,
    folderUnreadCounts,
    unseenInTabById,
    tabUnseenSenderLineById,
    listToolbarMode,
    compactInboxTabs,
    activeInboxTabId,
    pageIds,
    listRowsDep,
    effectiveRead,
    effectiveStarred,
    markAllInViewAsRead,
    requestDeleteScheduled,
    requestArchiveScheduled,
    requestSnoozeScheduled,
    requestToggleReadScheduled,
    requestRescheduleScheduled,
    requestSendScheduledNow,
    requestSnoozeMailboxEmail,
    requestRestoreSnoozedToInbox,
    isLoading,
    error,
    isFetching,
  }
}

export type EmailListData = ReturnType<typeof useEmailListData>
