import { Feather } from '@expo/vector-icons'
import type { BottomSheetModal } from '@gorhom/bottom-sheet'
import { FlashList } from '@shopify/flash-list'
import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query'
import { BlurView } from 'expo-blur'
import { type ErrorBoundaryProps, router } from 'expo-router'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Pressable, StyleSheet } from 'react-native'
import Animated, {
  measure,
  runOnUI,
  useAnimatedRef,
  useAnimatedScrollHandler,
} from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { ErrorState } from 'src/components/common/ErrorState'
import { FadeEdges } from 'src/components/layout/ScreenFadeEdges'
import { OrganizerAccountRow } from 'src/components/lists/OrganizerAccountRow'
import { OrganizerDragPreview } from 'src/components/lists/OrganizerDragPreview'
import { OrganizerListToken } from 'src/components/lists/OrganizerListToken'
import { OrganizerQuickLookSheet } from 'src/components/lists/OrganizerQuickLookSheet'
import {
  OrganizerDragProvider,
  useOrganizerDrag,
} from 'src/components/lists/organizerDrag'
import { useAddListMembersMutation } from 'src/hooks/mutations/useAddListMembersMutation'
import type { MastodonList } from 'src/lib/api/mastodon'
import type { Account } from 'src/lib/api-types'
import { haptic } from 'src/lib/haptics'
import { getListColor, reconcileListColorIndices } from 'src/lib/lists/listColors'
import { addMembershipLocal, type MembershipMap } from 'src/lib/lists/membershipMap'
import { enqueueAddToList, flushPendingAddToList } from 'src/lib/lists/organizerDebounce'
import { accountQueries } from 'src/lib/queries/accounts'
import { queryKeys } from 'src/lib/queries/keys'
import { listsQueries } from 'src/lib/queries/lists'
import { useActiveProfile, useAuth } from 'src/state/AuthProvider'
import {
  getListColorIndices,
  getOrganizerLastFilter,
  type OrganizerFilter,
  setListColorIndices,
  setOrganizerLastFilter,
} from 'src/state/storage'
import { Spinner, Text, useTheme, View, XStack, YStack } from 'tamagui'

// List Organizer (Surface E) — the bulk "sort 600 follows into 8 lists in five
// minutes" tool. A flexed left column of every follow (FlashList), a fixed
// right rail of colour tokens (the lists), and a long-press-then-drag gesture
// that drops accounts onto lists with optimistic, debounced writes. Mastodon
// only — Pixelfed's /api/v1/lists is a stub.

export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) {
  return <ErrorState message={error.message} onRetry={retry} />
}

const RAIL_WIDTH = 120
const EMPTY: string[] = []
// Stable empty map so `memberships` keeps a constant reference until the
// membership-map query resolves — avoids needless row re-memoisation pre-load.
const EMPTY_MAP: MembershipMap = {}

// FlashList wrapped for reanimated's worklet scroll handler + scrollTo
// auto-scroll. v2 forwards ScrollView props (onScroll) and a ref that
// scrollTo can drive (plan §13.1). The cast restores the generic component
// type that `createAnimatedComponent` erases.
const AnimatedFlashList = Animated.createAnimatedComponent(
  FlashList
) as unknown as typeof FlashList

function relativeAge(ts: number): string {
  if (!ts) return ''
  const mins = Math.floor((Date.now() - ts) / 60000)
  if (mins < 1) return 'Updated just now'
  if (mins < 60) return `Updated ${mins}m ago`
  const hrs = Math.floor(mins / 60)
  return `Updated ${hrs}h ago`
}

// Eagerly load all follow pages on mount — the organizer needs the full set to
// sort alphabetically and to compute "listless".
function useAllFollows(selfId: string, enabled: boolean) {
  const q = useInfiniteQuery({
    ...accountQueries.following(selfId),
    enabled: enabled && selfId.length > 0,
  })
  useEffect(() => {
    if (q.hasNextPage && !q.isFetchingNextPage) void q.fetchNextPage()
  }, [q.hasNextPage, q.isFetchingNextPage, q.data])

  const follows = useMemo(() => {
    const pages = (q.data?.pages ?? []) as { data: Account[] }[]
    const flat = pages.flatMap((p) => p.data)
    return [...flat].sort((a, b) =>
      (a.display_name || a.acct)
        .toLowerCase()
        .localeCompare((b.display_name || b.acct).toLowerCase())
    )
  }, [q.data])

  return { follows, query: q, fullyLoaded: !q.hasNextPage }
}

export default function OrganizeScreen() {
  const { user } = useAuth()
  const { profile } = useActiveProfile()
  const isMastodon = user?.type === 'mastodon'
  const accountId = user?.accountId ?? ''
  const selfId = profile?.id ?? ''

  const queryClient = useQueryClient()

  const listsQ = useQuery({ ...listsQueries.lists(), enabled: isMastodon })
  const lists = useMemo(() => listsQ.data ?? [], [listsQ.data])
  const listIdsKey = lists.map((l) => l.id).join('|')
  const listIds = useMemo(() => lists.map((l) => l.id), [listIdsKey])

  // Per-account OKLab colour assignments, reconciled against the catalogue.
  const [colorIndices, setColorIndices] = useState<Record<string, number>>(() =>
    accountId ? getListColorIndices(accountId) : {}
  )
  useEffect(() => {
    if (!accountId) return
    const reconciled = reconcileListColorIndices(
      lists.map((l) => l.id),
      getListColorIndices(accountId)
    )
    setColorIndices(reconciled)
    setListColorIndices(accountId, reconciled)
  }, [accountId, listIdsKey])

  const { follows, query: followingQ, fullyLoaded } = useAllFollows(selfId, !!isMastodon)

  // Derived `accountId → listIds[]` map — a persisted query (see
  // `listsQueries.membershipMap`) so the per-list fan-out only runs on the
  // first cold open; repeat opens hydrate instantly from the on-disk cache and
  // refetch in the background once stale.
  const membershipQ = useQuery({
    ...listsQueries.membershipMap(listIds),
    enabled: !!isMastodon && listIds.length > 0,
  })
  const memberships = membershipQ.data ?? EMPTY_MAP

  // Mutual-follow badges. Fire once the full follow set is loaded so the batch
  // key doesn't churn per page.
  const followIds = useMemo(() => follows.map((f) => f.id), [follows])
  const relQ = useQuery({
    ...accountQueries.relationshipsBatch(followIds),
    enabled: !!isMastodon && fullyLoaded && followIds.length > 0,
  })
  const mutuals = useMemo(() => {
    const set = new Set<string>()
    for (const r of relQ.data ?? []) if (r.followed_by === true) set.add(r.id)
    return set
  }, [relQ.data])

  const accountByIdRef = useRef(new Map<string, Account>())
  accountByIdRef.current = useMemo(() => {
    const m = new Map<string, Account>()
    for (const f of follows) m.set(f.id, f)
    return m
  }, [follows])

  // Live mirror of memberships so commitDrop can read it without becoming a
  // new function every time a drop mutates the map (which would otherwise
  // rebuild every row's gesture mid-drag).
  const membershipsRef = useRef(memberships)
  membershipsRef.current = memberships

  // Filter — remembered per account.
  const [filter, setFilter] = useState<OrganizerFilter>(() =>
    accountId ? getOrganizerLastFilter(accountId) : 'all'
  )
  const onFilterChange = useCallback(
    (next: OrganizerFilter) => {
      setFilter(next)
      if (accountId) setOrganizerLastFilter(accountId, next)
      haptic.selection()
    },
    [accountId]
  )

  const visibleFollows = useMemo(() => {
    if (filter === 'all') return follows
    return follows.filter((f) => (memberships[f.id] ?? EMPTY).length === 0)
  }, [filter, follows, memberships])

  // Stable callbacks (read live data via refs) so the drag provider's context
  // value — and therefore every row's composed gesture — doesn't rebuild as
  // follows / memberships stream in.
  const addMembers = useAddListMembersMutation()
  const addMembersRef = useRef(addMembers)
  addMembersRef.current = addMembers

  const sheetRef = useRef<BottomSheetModal | null>(null)
  const [quickLookAccount, setQuickLookAccount] = useState<Account | null>(null)

  const onQuickLook = useCallback((account: Account) => {
    setQuickLookAccount(account)
    sheetRef.current?.present()
  }, [])

  const commitDrop = useCallback(
    (droppedAccountId: string, listId: string) => {
      // Already a member? No-op (avoids a redundant POST + duplicate dot).
      const current = membershipsRef.current[droppedAccountId] ?? EMPTY
      if (current.includes(listId)) return
      // Optimistic, instant dot straight into the persisted map cache; the
      // debounced network POST follows. If that write ever fails the next
      // refetch (staleTime / pull-to-refresh) reconciles it — same
      // "optimistic, heals on refresh" behaviour the surface had with its old
      // local-state map.
      queryClient.setQueriesData<MembershipMap>(
        { queryKey: queryKeys.lists.membershipMapPrefix() },
        (prev) => addMembershipLocal(prev ?? {}, droppedAccountId, listId)
      )
      haptic.success()
      enqueueAddToList(listId, droppedAccountId, (ids) =>
        addMembersRef.current.mutateAsync({
          listId,
          accountIds: ids,
          accounts: ids
            .map((id) => accountByIdRef.current.get(id))
            .filter((a): a is Account => !!a),
        })
      )
    },
    [queryClient]
  )

  // Flush any debounced write if the user leaves mid-batch.
  useEffect(() => () => flushPendingAddToList(), [])

  const onRefresh = useCallback(() => {
    void listsQ.refetch()
    void followingQ.refetch()
    void relQ.refetch()
    void membershipQ.refetch()
  }, [listsQ, followingQ, relQ, membershipQ])

  // One settle-then-interact gate: hold the follows column on a spinner until
  // the catalogue, every follow page, and the membership map are all in — so
  // the list mounts once, fully sorted, dots already present, instead of rows
  // leaping as pages stream in and re-sort. Mutual-star badges (`relQ`) are
  // deliberately excluded: they layer onto avatars with no reflow and can't
  // even start until follows finish, so gating on them would only add
  // cold-load latency for a cosmetic badge.
  const mapReady = listIds.length === 0 || membershipQ.data !== undefined
  const ready = !!isMastodon && !listsQ.isPending && fullyLoaded && mapReady

  if (!isMastodon) {
    return (
      <YStack flex={1} backgroundColor="$background" px="$5" pt="$6" gap="$2">
        <Text fontSize="$body" color="$colorSecondary">
          Lists are a Mastodon-only feature. Switch to a Mastodon account to organize them
          here.
        </Text>
      </YStack>
    )
  }

  if (listsQ.isError) {
    return (
      <YStack flex={1} backgroundColor="$background" px="$5" pt="$6">
        <ErrorState message={listsQ.error.message} onRetry={listsQ.refetch} />
      </YStack>
    )
  }

  return (
    <OrganizerDragProvider
      onQuickLook={onQuickLook}
      onCommitDrop={commitDrop}
      colorIndices={colorIndices}
    >
      <OrganizeBody
        visibleFollows={visibleFollows}
        lists={lists}
        memberships={memberships}
        mutuals={mutuals}
        colorIndices={colorIndices}
        filter={filter}
        onFilterChange={onFilterChange}
        onRefresh={onRefresh}
        refreshing={listsQ.isRefetching || membershipQ.isRefetching}
        cacheAge={relativeAge(membershipQ.dataUpdatedAt || listsQ.dataUpdatedAt)}
        ready={ready}
      />
      <OrganizerQuickLookSheet
        ref={sheetRef}
        account={quickLookAccount}
        onDismiss={() => setQuickLookAccount(null)}
      />
    </OrganizerDragProvider>
  )
}

type BodyProps = {
  visibleFollows: Account[]
  lists: MastodonList[]
  memberships: MembershipMap
  mutuals: Set<string>
  colorIndices: Record<string, number>
  filter: OrganizerFilter
  onFilterChange: (next: OrganizerFilter) => void
  onRefresh: () => void
  refreshing: boolean
  cacheAge: string
  ready: boolean
}

function OrganizeBody({
  visibleFollows,
  lists,
  memberships,
  mutuals,
  colorIndices,
  filter,
  onFilterChange,
  onRefresh,
  refreshing,
  cacheAge,
  ready,
}: BodyProps) {
  const theme = useTheme()
  const insets = useSafeAreaInsets()
  const { leftListRef, leftScrollHandler, railScrollY, containerX, containerY } =
    useOrganizerDrag()

  const railScrollHandler = useAnimatedScrollHandler((e) => {
    railScrollY.value = e.contentOffset.y
  })

  // Capture the body's page-coordinate origin so the drag preview can map
  // screen-space finger coords back into its local space (it sits below the
  // native header).
  const rootRef = useAnimatedRef<Animated.View>()
  const measureRoot = useCallback(() => {
    runOnUI(() => {
      'worklet'
      const m = measure(rootRef)
      if (m) {
        containerX.value = m.pageX
        containerY.value = m.pageY
      }
    })()
  }, [rootRef, containerX, containerY])

  const renderItem = useCallback(
    ({ item }: { item: Account }) => (
      <OrganizerAccountRow
        account={item}
        listIds={memberships[item.id] ?? EMPTY}
        isMutual={mutuals.has(item.id)}
        colorIndices={colorIndices}
      />
    ),
    [memberships, mutuals, colorIndices]
  )

  const onTapToken = useCallback((listId: string) => {
    router.push(`/settings/lists/${listId}` as any)
  }, [])

  // No native header — the columns run edge-to-edge, so the screen owns its
  // top inset. Both columns start their content at the same y below the
  // status bar / notch.
  const topInset = insets.top + 16
  const bottomInset = insets.bottom + 12

  return (
    <Animated.View
      ref={rootRef}
      onLayout={measureRoot}
      testID="organize-screen"
      style={{ flex: 1, backgroundColor: theme.background.val }}
    >
      <XStack flex={1}>
        {/* Left column — follows */}
        <View flex={1}>
          {!ready ? (
            <YStack flex={1} alignItems="center" justifyContent="center">
              <Spinner color={theme.color.val} />
            </YStack>
          ) : (
            <AnimatedFlashList
              ref={leftListRef as never}
              onScroll={leftScrollHandler}
              scrollEventThrottle={16}
              data={visibleFollows}
              keyExtractor={(item: Account) => item.id}
              renderItem={renderItem}
              refreshing={refreshing}
              onRefresh={onRefresh}
              contentContainerStyle={{
                paddingTop: topInset,
                paddingBottom: bottomInset + 64,
              }}
              ListEmptyComponent={
                <YStack px="$5" pt="$8" alignItems="center" gap="$2">
                  <Text fontSize="$body" color="$colorSecondary" textAlign="center">
                    {filter === 'listless'
                      ? 'Every follow is already in a list.'
                      : 'No follows yet. Find accounts to follow from the Explore tab.'}
                  </Text>
                </YStack>
              }
            />
          )}
        </View>

        {/* Divider */}
        <View width={StyleSheet.hairlineWidth} backgroundColor="$borderColor" />

        {/* Right rail — list tokens */}
        <View width={RAIL_WIDTH}>
          {lists.length === 0 ? (
            <YStack flex={1} alignItems="center" justifyContent="center" px="$3">
              <Text fontSize="$caption" color="$colorSecondary" textAlign="center">
                Tap + to create your first list.
              </Text>
            </YStack>
          ) : (
            <Animated.ScrollView
              onScroll={railScrollHandler}
              scrollEventThrottle={16}
              showsVerticalScrollIndicator={false}
              contentContainerStyle={{
                alignItems: 'center',
                paddingTop: topInset,
                paddingBottom: bottomInset + 72,
                gap: 12,
              }}
            >
              {lists.map((list) => (
                <OrganizerListToken
                  key={list.id}
                  list={list}
                  color={getListColor(list.id, colorIndices)}
                  onPress={onTapToken}
                />
              ))}
            </Animated.ScrollView>
          )}
        </View>
      </XStack>

      {/* Same perceptually-eased fade the feed uses, sized to this screen's
          edge-to-edge chrome: the top dissolves content under the status bar /
          notch, the bottom carries it into the glass capsules. Both layers are
          pointerEvents='none', and they paint below the capsules + drag preview
          rendered after them. */}
      <FadeEdges topHeight={topInset} bottomHeight={bottomInset + 84} />

      {/* Bottom glass bars — taps fall through to the lists except on capsules */}
      <View
        position="absolute"
        left={0}
        right={0}
        bottom={bottomInset}
        pointerEvents="box-none"
      >
        <XStack px="$3" alignItems="center" justifyContent="space-between">
          <GlassCapsule>
            <XStack alignItems="center" gap="$2">
              <FilterToggle value={filter} onChange={onFilterChange} />
              {refreshing ? (
                <Spinner size="small" color={theme.colorSecondary.val} />
              ) : cacheAge ? (
                <Text fontSize="$micro" color="$onSurfaceMuted">
                  {cacheAge}
                </Text>
              ) : null}
              <Pressable
                onPress={onRefresh}
                hitSlop={8}
                accessibilityRole="button"
                accessibilityLabel="Refresh"
              >
                <Feather name="refresh-cw" size={16} color={theme.onSurfaceMuted.val} />
              </Pressable>
            </XStack>
          </GlassCapsule>

          <GlassCapsule>
            <Pressable
              onPress={() => router.push('/settings/lists/new' as any)}
              hitSlop={8}
              accessibilityRole="button"
              accessibilityLabel="New list"
            >
              <Feather name="plus" size={20} color={theme.onSurfaceMuted.val} />
            </Pressable>
          </GlassCapsule>
        </XStack>
      </View>

      <OrganizerDragPreview />
    </Animated.View>
  )
}

function GlassCapsule({ children }: { children: React.ReactNode }) {
  return (
    <View borderRadius="$10" overflow="hidden">
      <BlurView intensity={24} tint="default" style={StyleSheet.absoluteFill} />
      <View style={StyleSheet.absoluteFill} backgroundColor="$surfaceGlassTint" />
      <XStack px="$3" py="$2" alignItems="center">
        {children}
      </XStack>
    </View>
  )
}

function FilterToggle({
  value,
  onChange,
}: {
  value: OrganizerFilter
  onChange: (next: OrganizerFilter) => void
}) {
  const options: OrganizerFilter[] = ['all', 'listless']
  return (
    <XStack borderRadius="$10" overflow="hidden" backgroundColor="$backgroundTertiary">
      {options.map((opt) => {
        const active = value === opt
        return (
          <Pressable key={opt} onPress={() => onChange(opt)} accessibilityRole="button">
            <View
              paddingHorizontal={12}
              paddingVertical={6}
              borderRadius="$10"
              backgroundColor={active ? '$accent' : 'transparent'}
            >
              <Text
                fontSize="$caption"
                fontWeight={active ? '700' : '500'}
                color={active ? '$onAccent' : '$onSurfaceMuted'}
              >
                {opt === 'all' ? 'All' : 'Listless'}
              </Text>
            </View>
          </Pressable>
        )
      })}
    </XStack>
  )
}
