import type { BottomTabBarProps } from '@react-navigation/bottom-tabs'
import { LinearGradient } from '@tamagui/linear-gradient'
import { useQuery } from '@tanstack/react-query'
import { useSegments } from 'expo-router'
import { useCallback, useMemo } from 'react'
import { Platform, Pressable, StyleSheet } from 'react-native'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { haptic } from 'src/lib/haptics'
import { notificationQueries } from 'src/lib/queries/notifications'
import { useAuth } from 'src/state/AuthProvider'
import type { AccountType } from 'src/state/accountStore'
import { Text, useTheme, View, XStack, YStack } from 'tamagui'
import { useA11yFlags, useKeyboardVisible } from './hooks'
import { placeIntents } from './intents'
import {
  BUTTON_SIZE,
  CONTEXT_ICON_SIZE,
  CONTEXT_ICON_SIZE_BOLD,
  ICON_SIZE,
  ICON_SIZE_BOLD,
  MenuBarFloating,
  MenuIcon,
  MenuSurfacePill,
} from './MenuBar'
import { useActiveMenuEntry } from './MenuStore'
import { RenderAct, RenderCommit } from './render/renderAct'
import { RenderBack } from './render/renderBack'
import { RenderFilter } from './render/renderFilter'
import { RenderMenu } from './render/renderMenu'
import { RenderNav } from './render/renderNav'
import { RenderSearchField, RenderSearchToggle } from './render/renderSearch'
import { RenderTitle } from './render/renderTitle'
import { scrollStore, useMinimized } from './scroll'
import { menuSelectableStyle, tabTone, useMenuSelectableTokens } from './selectable'
import { tabPressStore } from './tabPressStore'
import type { IconName } from './types'

// MenuBarUnified — the floating chrome for tab routes. Two physically
// separate pills inside one positional wrapper: a hug-to-content context
// pill anchored to the right, and a full-width tab strip below it.
//
// On scroll-down (or when there's no active tab context), the two collapse
// to a single full-width pill: left-anchored active-tab icon + the
// contextual actions. Tapping the active-tab icon uncollapses the bar
// (same effect as scrolling up); a horizontal swipe cycles tabs.

interface TabConfig {
  name: string
  icon: IconName
  iconActive?: IconName
  label: string
  // Camera keeps the filled-accent-pill identity inside the tab row so the
  // create-affordance reads at a glance.
  highlighted?: boolean
  // Optional numeric badge overlayed on the tab icon — e.g. unread
  // notifications on the bell tab. `0` or undefined hides the badge.
  badge?: number
}

// Single source of truth for the tab strip. Camera sits at the end — it's
// the create affordance and anchors the right edge. The camera-tab icon is
// the only per-account-type variation: a writing glyph on Mastodon (where
// the composer is text-first) and the camera glyph on Pixelfed (media-
// first). `send` (paper plane) is reserved for the per-screen "submit/post"
// affordance (see `PostComposer.tsx`'s commit button) and intentionally not
// reused as a tab affordance.
function getTabs(accountType: AccountType): TabConfig[] {
  return [
    { name: 'home', icon: 'home', label: 'Home' },
    { name: 'explore', icon: 'compass', label: 'Explore' },
    { name: 'notifications', icon: 'bell', label: 'Notifications' },
    { name: 'profile', icon: 'user', label: 'Profile' },
    {
      name: 'camera',
      icon: accountType === 'mastodon' ? 'edit-2' : 'camera',
      label: 'New post',
      highlighted: true,
    },
  ]
}

// Tab name order, used by the swipe gesture to cycle through tabs without
// caring about the camera-tab icon variant.
const TAB_ORDER = ['home', 'explore', 'notifications', 'profile', 'camera']

const TAB_BAR_TAB_SIZE = 44

export function TabBarTab({
  tab,
  focused,
  onPress,
  onLongPress,
  reduceMotion,
  boldText,
}: {
  tab: TabConfig
  focused: boolean
  onPress: () => void
  onLongPress?: () => void
  reduceMotion?: boolean
  boldText?: boolean
}) {
  const theme = useTheme()
  const tokens = useMenuSelectableTokens()
  const iconName = focused && tab.iconActive ? tab.iconActive : tab.icon
  const size = boldText ? ICON_SIZE_BOLD : ICON_SIZE
  // Unified selectable vocabulary. Highlighted tabs (Camera) stay visually
  // distinct via `selected` fill even when unfocused; the 1.05 scale is
  // reserved for the focused highlighted case to signal "this is current".
  const wrap = menuSelectableStyle(tabTone(focused, !!tab.highlighted), tokens)

  const badge = tab.badge && tab.badge > 0 ? tab.badge : 0
  const badgeLabel = badge >= 40 ? '40+' : String(badge)
  const a11yLabel = badge ? `${tab.label}, ${badgeLabel} unread` : tab.label

  // Highlighted (Camera) is the app's primary create affordance — it picks up
  // the full accent treatment: a vertical gradient fill, an accent-tinted
  // elevation halo on iOS (Android's elevation is always a neutral drop
  // shadow), and the same icon colour as `onAccent`.
  const isHighlighted = !!tab.highlighted
  const accent = theme.accent.val as string
  const accentPressed = theme.accentPressed.val as string
  const onAccent = theme.onAccent.val as string
  const accentGlow = theme.accentGlow.val as string

  return (
    <Pressable
      onPress={() => {
        if (!reduceMotion) haptic.selection()
        onPress()
      }}
      onLongPress={onLongPress}
      accessibilityRole="tab"
      accessibilityLabel={a11yLabel}
      accessibilityState={{ selected: focused }}
      hitSlop={6}
      style={({ pressed }) => ({
        width: TAB_BAR_TAB_SIZE,
        height: TAB_BAR_TAB_SIZE,
        alignItems: 'center',
        justifyContent: 'center',
        // The pill background and (highlighted) gradient live in an
        // absolutely-positioned clipped child below; the Pressable itself
        // stays `overflow: 'visible'` so the unread-count badge anchored to
        // its top-right corner doesn't get clipped by the pill's rounded
        // corner.
        overflow: 'visible',
        ...(isHighlighted
          ? {
              shadowColor: accentGlow,
              shadowOffset: { width: 0, height: 3 },
              shadowOpacity: 1,
              shadowRadius: 8,
              elevation: Platform.OS === 'android' ? 3 : 0,
            }
          : null),
        opacity: pressed ? 0.9 : 1,
        transform: [{ scale: pressed ? 0.96 : wrap.scale }],
      })}
    >
      <View
        position="absolute"
        top={0}
        left={0}
        right={0}
        bottom={0}
        borderRadius={TAB_BAR_TAB_SIZE / 2}
        overflow="hidden"
        backgroundColor={isHighlighted ? accent : wrap.backgroundColor}
        pointerEvents="none"
      >
        {isHighlighted ? (
          <LinearGradient
            colors={[accent, accentPressed]}
            start={[0, 0]}
            end={[0, 1]}
            style={StyleSheet.absoluteFill}
          />
        ) : null}
      </View>
      <MenuIcon
        name={iconName}
        size={size}
        tint={isHighlighted ? onAccent : wrap.iconTint}
      />
      {badge ? (
        <View
          position="absolute"
          top={2}
          right={2}
          borderRadius={999}
          minWidth={16}
          height={16}
          paddingHorizontal={4}
          justifyContent="center"
          alignItems="center"
          overflow="hidden"
          accessible={false}
          pointerEvents="none"
        >
          <LinearGradient
            colors={[accent, accentPressed]}
            start={[0, 0]}
            end={[0, 1]}
            style={StyleSheet.absoluteFill}
          />
          <Text
            color={onAccent}
            fontSize={10}
            fontWeight="700"
            allowFontScaling={false}
            lineHeight={12}
          >
            {badgeLabel}
          </Text>
        </View>
      ) : null}
    </Pressable>
  )
}

function TabStrip({
  tabs,
  state,
  onPress,
  onLongPress,
  reduceMotion,
  boldText,
}: {
  tabs: TabConfig[]
  state: BottomTabBarProps['state']
  onPress: (routeKey: string, routeName: string, focused: boolean) => void
  onLongPress: (routeKey: string) => void
  reduceMotion?: boolean
  boldText?: boolean
}) {
  const orderedRoutes = tabs
    .map((cfg) => state.routes.find((r) => r.name === cfg.name))
    .filter((r): r is (typeof state.routes)[number] => !!r)

  // Tab strip fills the floating bar — this is the primary affordance and
  // wants the hit-target to reach the full bar width, unlike the context
  // pill above which hugs to content.
  return (
    <XStack flex={1} justifyContent="space-around" alignItems="center">
      {orderedRoutes.map((route) => {
        const cfg = tabs.find((t) => t.name === route.name)
        if (!cfg) return null
        const index = state.routes.indexOf(route)
        const focused = state.index === index
        return (
          <TabBarTab
            key={route.key}
            tab={cfg}
            focused={focused}
            onPress={() => onPress(route.key, route.name, focused)}
            onLongPress={() => onLongPress(route.key)}
            reduceMotion={reduceMotion}
            boldText={boldText}
          />
        )
      })}
    </XStack>
  )
}

export function MenuBarUnified(props: BottomTabBarProps) {
  const { state, navigation } = props
  const insets = useSafeAreaInsets()
  const keyboardVisible = useKeyboardVisible()
  const a11y = useA11yFlags()
  const entry = useActiveMenuEntry()
  const segments = useSegments()
  const minimized = useMinimized()

  const onTabRoute = segments.some((s) => s === '(tabs)')
  // Collapsed mode only makes sense where the tab strip exists — detail
  // routes are already single-row.
  const collapsed = minimized && onTabRoute
  const placed = useMemo(() => (entry ? placeIntents(entry.spec.intents) : null), [entry])

  // Poll unread notifications in the foreground so the bell tab badge stays
  // current. See notificationQueries.unreadCount — marker + min_id count.
  const { data: unreadCount = 0 } = useQuery(notificationQueries.unreadCount())
  const accountType = useAuth().user?.type ?? 'pixelfed'
  const tabs = useMemo<TabConfig[]>(
    () =>
      getTabs(accountType).map((t) =>
        t.name === 'notifications' ? { ...t, badge: unreadCount } : t
      ),
    [accountType, unreadCount]
  )

  const handleTabPress = useCallback(
    (routeKey: string, routeName: string, focused: boolean) => {
      // Fan-out via tabPressStore — the screen's `useNavigation()` returns the
      // inner stack navigator under expo-router, so an `addListener('tabPress')`
      // on it never sees what the tabs navigator emits. Route-keyed pub/sub
      // sidesteps that entirely.
      tabPressStore.emit(routeName)
      if (focused) return
      const event = navigation.emit({
        type: 'tabPress',
        target: routeKey,
        canPreventDefault: true,
      })
      if (!event.defaultPrevented) {
        navigation.navigate(routeName as never)
      }
    },
    [navigation]
  )
  const handleTabLongPress = useCallback(
    (routeKey: string) => {
      navigation.emit({ type: 'tabLongPress', target: routeKey })
    },
    [navigation]
  )

  const activeCfg = useMemo(() => {
    const activeRoute = state.routes[state.index]
    if (!activeRoute) return null
    return tabs.find((t) => t.name === activeRoute.name) ?? null
  }, [state, tabs])

  // Horizontal swipe across the collapsed bar cycles tabs. Uses Gesture
  // Handler with explicit offsets so it composes with the inner Pressables
  // without eating taps. Disabled when expanded or keyboard is up.
  const swipeEnabled = collapsed && !keyboardVisible && onTabRoute
  const panGesture = useMemo(
    () =>
      Gesture.Pan()
        .activeOffsetX([-20, 20])
        .failOffsetY([-10, 10])
        .onEnd((e) => {
          if (!swipeEnabled) return
          const vx = e.velocityX
          const dx = e.translationX
          if (Math.abs(dx) < 40) return
          const dir: 'next' | 'prev' = dx < 0 || vx < 0 ? 'next' : 'prev'
          const ordered = TAB_ORDER.map((name) =>
            state.routes.findIndex((r) => r.name === name)
          ).filter((i) => i >= 0)
          const pos = ordered.indexOf(state.index)
          if (pos < 0) return
          const nextPos =
            dir === 'next'
              ? (pos + 1) % ordered.length
              : (pos - 1 + ordered.length) % ordered.length
          const nextRouteIndex = ordered[nextPos]
          const nextRoute = state.routes[nextRouteIndex]
          if (!nextRoute) return
          if (!a11y.reduceMotion) haptic.selection()
          handleTabPress(nextRoute.key, nextRoute.name, false)
        }),
    [swipeEnabled, state, handleTabPress, a11y.reduceMotion]
  )

  if (!onTabRoute && !entry) return null

  // When the keyboard is up we hide the bar unless the current screen's
  // search intent opts in to staying visible AND search is expanded.
  if (keyboardVisible) {
    const keepOnKeyboard = !!placed?.search?.keepVisibleOnKeyboard && entry?.searchActive
    if (!keepOnKeyboard && !entry?.searchActive) return null
  }

  const surface = a11y.reduceTransparency ? 'solid' : 'translucent'
  const bottomOffset = insets.bottom + 8

  const hasContext = !!(
    placed &&
    (placed.back ||
      placed.filter ||
      placed.search ||
      placed.trailing.length > 0 ||
      placed.commit ||
      placed.menu ||
      entry?.spec.title)
  )

  const showSearchField =
    !!placed?.search && !!entry?.searchActive && placed.search.mode !== 'route'

  const renderTabStrip = onTabRoute

  return (
    <MenuBarFloating bottom={bottomOffset}>
      <YStack
        gap="$2"
        transition="quick"
        animateOnly={['transform', 'opacity']}
        enterStyle={{ opacity: 0, scale: 0.98 }}
        exitStyle={{ opacity: 0, scale: 0.98 }}
      >
        {collapsed ? (
          <GestureDetector gesture={panGesture}>
            <MenuSurfacePill surface={surface}>
              <CollapsedRow
                placed={placed}
                entry={entry}
                activeCfg={activeCfg}
                a11y={a11y}
                showSearchField={showSearchField}
              />
            </MenuSurfacePill>
          </GestureDetector>
        ) : (
          <>
            {hasContext ? (
              <XStack justifyContent="flex-end">
                <MenuSurfacePill surface={surface} hug compact>
                  <ContextRow
                    placed={placed}
                    entry={entry}
                    a11y={a11y}
                    showSearchField={showSearchField}
                  />
                </MenuSurfacePill>
              </XStack>
            ) : null}
            {renderTabStrip ? (
              <GestureDetector gesture={panGesture}>
                <MenuSurfacePill surface={surface} paddingHorizontal="$3">
                  <TabStrip
                    tabs={tabs}
                    state={state}
                    onPress={handleTabPress}
                    onLongPress={handleTabLongPress}
                    reduceMotion={a11y.reduceMotion}
                    boldText={a11y.boldText}
                  />
                </MenuSurfacePill>
              </GestureDetector>
            ) : null}
          </>
        )}
      </YStack>
    </MenuBarFloating>
  )
}

function ContextRow({
  placed,
  entry,
  a11y,
  showSearchField,
}: {
  placed: ReturnType<typeof placeIntents> | null
  entry: ReturnType<typeof useActiveMenuEntry>
  a11y: ReturnType<typeof useA11yFlags>
  showSearchField: boolean
}) {
  const theme = useTheme()
  if (!placed || !entry) return null
  const { back, filter, search, trailing, commit, menu } = placed
  const { spec, toggleSearch } = entry
  // Chrome icons (back, search toggle, trailing nav/act) share the
  // `$onSurfaceMuted` tint with the filter icons' rest state so a context
  // pill mixing a filter + trailing actions — e.g. notifications' icon
  // filter + DM envelope — reads as one visual weight instead of a heavy
  // envelope next to lighter filter glyphs.
  const tint = theme.onSurfaceMuted.val as string
  const size = a11y.boldText ? CONTEXT_ICON_SIZE_BOLD : CONTEXT_ICON_SIZE
  return (
    <>
      {back ? (
        <RenderBack
          intent={back}
          reduceMotion={a11y.reduceMotion}
          size={size}
          tint={tint}
        />
      ) : null}

      {showSearchField && search ? (
        <RenderSearchField
          intent={search}
          onClose={toggleSearch}
          reduceMotion={a11y.reduceMotion}
          size={size}
          tint={tint}
        />
      ) : (
        <>
          <MiddleSlot title={spec.title} filter={filter} boldText={a11y.boldText} />
          {search ? (
            <RenderSearchToggle
              intent={search}
              onToggle={toggleSearch}
              reduceMotion={a11y.reduceMotion}
              size={size}
              tint={tint}
            />
          ) : null}
          {trailing.length > 0 ? (
            <>
              {filter ? (
                <View
                  width={1}
                  height={20}
                  backgroundColor="$borderColor"
                  marginHorizontal="$2"
                  opacity={0.6}
                />
              ) : null}
              <XStack gap="$1">
                {trailing.map((item) =>
                  item.kind === 'act' ? (
                    <RenderAct
                      key={item.id}
                      intent={item}
                      reduceMotion={a11y.reduceMotion}
                      size={size}
                      tint={tint}
                    />
                  ) : (
                    <RenderNav
                      key={item.id ?? item.label}
                      intent={item}
                      reduceMotion={a11y.reduceMotion}
                      size={size}
                      tint={tint}
                    />
                  )
                )}
              </XStack>
            </>
          ) : null}
          {menu ? (
            <RenderMenu
              intent={menu}
              reduceMotion={a11y.reduceMotion}
              boldText={a11y.boldText}
            />
          ) : null}
          {commit ? (
            <RenderCommit
              intent={commit}
              reduceMotion={a11y.reduceMotion}
              boldText={a11y.boldText}
            />
          ) : null}
        </>
      )}
    </>
  )
}

function CollapsedRow({
  placed,
  entry,
  activeCfg,
  a11y,
  showSearchField,
}: {
  placed: ReturnType<typeof placeIntents> | null
  entry: ReturnType<typeof useActiveMenuEntry>
  activeCfg: TabConfig | null
  a11y: ReturnType<typeof useA11yFlags>
  showSearchField: boolean
}) {
  const theme = useTheme()
  const tokens = useMenuSelectableTokens()
  // Same muted-chrome tint as ContextRow so the collapsed pill's trailing
  // actions read as a coherent set with any filter icons to their left.
  const tint = theme.onSurfaceMuted.val as string
  const size = a11y.boldText ? ICON_SIZE_BOLD : ICON_SIZE

  if (!activeCfg) return null

  // Tapping the indicator uncollapses the bar — same effect as scrolling up.
  // It also fires the active tab's tab-press event so screens that subscribe
  // via `useTabPressScrollToTop` get the same scroll-to-top behaviour they'd
  // get from re-tapping the active tab in the expanded state. Without this
  // the collapsed indicator would silently skip the scroll-to-top branch (no
  // TabStrip is rendered while collapsed, so `handleTabPress` is unreachable).
  const onIndicatorPress = () => {
    if (!a11y.reduceMotion) haptic.selection()
    if (activeCfg) tabPressStore.emit(activeCfg.name)
    scrollStore.setMinimized(false)
  }

  // The collapsed-row indicator always represents the *current* tab, so it's
  // inherently focused. Highlighted (camera) draws as a filled pill; otherwise
  // as a coloured icon on transparent — same treatment as the tab strip.
  const indicatorStyle = menuSelectableStyle(
    tabTone(true, !!activeCfg.highlighted),
    tokens
  )

  const indicator = (
    <Pressable
      onPress={onIndicatorPress}
      accessibilityRole="button"
      accessibilityLabel="Expand menu"
      accessibilityHint="Shows the full menu"
      hitSlop={8}
      style={({ pressed }) => ({
        width: BUTTON_SIZE,
        height: BUTTON_SIZE,
        borderRadius: BUTTON_SIZE / 2,
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: indicatorStyle.backgroundColor,
        opacity: pressed ? 0.7 : 1,
        transform: [{ scale: indicatorStyle.scale }],
      })}
    >
      <MenuIcon name={activeCfg.icon} size={size} tint={indicatorStyle.iconTint} />
    </Pressable>
  )

  const { back, filter, search, trailing, commit, menu } = placed ?? {
    back: undefined,
    filter: undefined,
    search: undefined,
    trailing: [],
    commit: undefined,
    menu: undefined,
  }
  const toggleSearch = entry?.toggleSearch ?? (() => {})

  // The controls group is absolutely positioned to the pill's right edge —
  // every flex-based approach to space-between failed because the horizontal
  // ScrollView inside MenuSegmentsRow (home's filter) is greedy and pulls
  // whatever XStack wraps it to the full pill width. Yoga then sees a single
  // full-width flex sibling and has no free space to distribute via
  // `justifyContent: 'space-between'`, `marginLeft: 'auto'`, or a flex-grow
  // spacer — the group hits the pill's right edge but its own content
  // left-packs inside those too-wide bounds. `position: 'absolute'` pulls
  // the controls out of flex flow entirely: the pill's flex layout sees
  // only the indicator (left-anchored naturally), and the controls sit
  // wherever we pin them regardless of their intrinsic sizing.
  //
  // `right: 0` places the right edge of the absolute XStack at the pill's
  // inner right (padding-box), mirroring the indicator's 8pt paddingLeft so
  // both sides respect the same pill-edge inset. The search-expanded branch
  // is unaffected — the search field stays in the flex flow and fills the
  // pill via its own `flexGrow: 1` after the indicator.
  //
  // `maxWidth: '70%'` bounds the absolute container's leftward reach so the
  // inner `RenderFilter` ScrollView can never grow past 70% of the pill —
  // RN clips its viewport at the parent's maxWidth and the ScrollView's
  // own scroll behaviour kicks in for overflow inside that viewport. The
  // indicator at the pill's left always retains ≥30% clear width. With
  // larger Mastodon Lists pill counts the filter scrolls instead of
  // overlapping the indicator. Static cap rather than measured because the
  // indicator's intrinsic width is well under 30% in both collapsed and
  // expanded label states; revisit if a future indicator design pushes
  // past that.
  return (
    <>
      {indicator}

      {showSearchField && search ? (
        <RenderSearchField
          intent={search}
          onClose={toggleSearch}
          reduceMotion={a11y.reduceMotion}
          size={size}
          tint={tint}
        />
      ) : (
        <XStack
          position="absolute"
          right={0}
          top={0}
          bottom={0}
          paddingRight="$2"
          alignItems="center"
          gap="$1"
          maxWidth="70%"
        >
          {back ? (
            <RenderBack
              intent={back}
              reduceMotion={a11y.reduceMotion}
              size={size}
              tint={tint}
            />
          ) : null}
          {filter ? <RenderFilter intent={filter} boldText={a11y.boldText} /> : null}
          {search ? (
            <RenderSearchToggle
              intent={search}
              onToggle={toggleSearch}
              reduceMotion={a11y.reduceMotion}
              size={size}
              tint={tint}
            />
          ) : null}
          {trailing.length > 0 ? (
            <>
              {filter ? (
                <View
                  width={1}
                  height={20}
                  backgroundColor="$borderColor"
                  marginHorizontal="$2"
                  opacity={0.6}
                />
              ) : null}
              <XStack gap="$1">
                {trailing.map((item) =>
                  item.kind === 'act' ? (
                    <RenderAct
                      key={item.id}
                      intent={item}
                      reduceMotion={a11y.reduceMotion}
                      size={size}
                      tint={tint}
                    />
                  ) : (
                    <RenderNav
                      key={item.id ?? item.label}
                      intent={item}
                      reduceMotion={a11y.reduceMotion}
                      size={size}
                      tint={tint}
                    />
                  )
                )}
              </XStack>
            </>
          ) : null}
          {menu ? (
            <RenderMenu
              intent={menu}
              reduceMotion={a11y.reduceMotion}
              boldText={a11y.boldText}
            />
          ) : null}
          {commit ? (
            <RenderCommit
              intent={commit}
              reduceMotion={a11y.reduceMotion}
              boldText={a11y.boldText}
            />
          ) : null}
        </XStack>
      )}
    </>
  )
}

function MiddleSlot({
  title,
  filter,
  boldText,
}: {
  title?: string
  filter?: ReturnType<typeof placeIntents>['filter']
  boldText?: boolean
}) {
  if (filter) {
    return <RenderFilter intent={filter} boldText={boldText} />
  }
  if (title) {
    return (
      <View flexGrow={1} flexShrink={1} alignItems="center" justifyContent="center">
        <RenderTitle title={title} boldText={boldText} />
      </View>
    )
  }
  return null
}
