import '@tamagui/native/setup-gesture-handler'
import { setupGestureHandler } from '@tamagui/native/gesture-handler'
import { Platform as _RNPlatformForGesture } from 'react-native'

if (_RNPlatformForGesture.OS === 'android') {
  setupGestureHandler({ pressEvents: false })
}
import '@tamagui/native/setup-teleport'
import '@tamagui/native/setup-expo-linear-gradient'
import '@tamagui/native/setup-burnt'
import '@tamagui/native/setup-zeego'
import Feather from '@expo/vector-icons/Feather'
import {
  Inter_400Regular,
  Inter_500Medium,
  Inter_600SemiBold,
  Inter_700Bold,
  Inter_800ExtraBold,
} from '@expo-google-fonts/inter'
import { JetBrainsMono_400Regular } from '@expo-google-fonts/jetbrains-mono'
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'
import { focusManager, QueryClientProvider, useQuery } from '@tanstack/react-query'
import { useFonts } from 'expo-font'
import * as NavigationBar from 'expo-navigation-bar'
import * as Notifications from 'expo-notifications'
import { Stack, useRouter } from 'expo-router'
import { ShareIntentProvider } from 'expo-share-intent'
import * as SplashScreen from 'expo-splash-screen'
import { StatusBar } from 'expo-status-bar'
import * as SystemUI from 'expo-system-ui'
import { type ReactNode, useEffect, useRef, useSyncExternalStore } from 'react'
import { type AppStateStatus, Platform } from 'react-native'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import {
  initialWindowMetrics,
  SafeAreaProvider,
  useSafeAreaInsets,
} from 'react-native-safe-area-context'
import { AccountSwitchOverlay } from 'src/components/AccountSwitchOverlay'
import ImageViewerOverlay from 'src/components/common/ImageViewerOverlay'
import { DevSeedListener } from 'src/components/DevSeedListener'
import { MenuBarToolbar, TAB_BAR_HEIGHT } from 'src/components/menu'
import GlobalToast from 'src/components/notifications/GlobalToast'
import { PermissionDisclosure } from 'src/components/PermissionDisclosure'
import { useAppState } from 'src/hooks/useAppState'
import { ImageViewerProvider } from 'src/hooks/useImageViewer'
import { useOnlineManager } from 'src/hooks/useOnlineManager'
import { useStoredTheme } from 'src/hooks/useStoredTheme'
import type { AccentVariantId } from 'src/lib/accentVariants'
import { getMastodonNotification } from 'src/lib/api/mastodon'
import { applyAudioSession } from 'src/lib/audio/session'
import { installNetLog } from 'src/lib/devnet/netlog'
import {
  applyImageCacheCap,
  applyVideoCacheCap,
  maybeAutoEvict,
} from 'src/lib/media/cacheConfig'
import { queryClient } from 'src/lib/queries/client'
import { notificationQueries } from 'src/lib/queries/notifications'
import {
  attachAccountSwitchListener,
  attachAppStateListener,
} from 'src/lib/tapestry/emit'
import WatchHandoffBridge from 'src/lib/watch/WatchHandoffBridge'
import type { ThemeName } from 'tamagui'
import { TamaguiProvider, Theme, ToastProvider, ToastViewport, useTheme } from 'tamagui'
import { config } from '../../tamagui.config'
import AuthProvider, { useAuth } from '../state/AuthProvider'
import { AccountStore } from '../state/accountStore'
import { bootCleanupV1 } from '../state/bootCleanup'
import { cleanupStaleThemeIfNeeded } from '../state/storage'

export const unstable_settings = {
  backBehavior: 'history',
}

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: false,
    // Foregrounded pushes don't carry a count yet (backend P16 would add
    // one); rely on the polled unread-count query to drive the OS badge.
    shouldSetBadge: true,
    shouldShowBanner: true,
    shouldShowList: true,
  }),
})

// Targeted LogBox suppressions instead of blanket ignore.
// Add specific warnings here as needed:
// LogBox.ignoreLogs(['Warning message substring'])

// One-shot wipe of legacy single-account state on first launch of the
// multi-account build. Halflight has not been deployed publicly, so the
// dev team's existing local installs are acceptable casualties — they will
// re-login once. Gated by `schema.v1.cleared` so it fires exactly once.
bootCleanupV1()
cleanupStaleThemeIfNeeded()

// P3b/P3c — apply media-cache caps once at boot. Both are best-effort; the
// underlying platforms also bound growth via their own LRUs. The image cap
// is iOS-only (Android Coil exposes no programmatic API).
// P3e — fire periodic auto-wipe alongside the caps. Default policy is
// 'never' so this is inert until the user opts in via Settings → Storage.
// Video-playback Phase 1 — pin the audio session category before any
// `useVideoPlayer` mounts so background-audio + per-player ducking work
// deterministically across iOS/Android.
applyAudioSession()
applyVideoCacheCap()
applyImageCacheCap()
maybeAutoEvict()

// Dev-only fetch wrapper that streams every HTTP request as a tagged
// `[net]` line into Metro stdout (and sinks JSON bodies under 256 KB to
// Paths.document/devnet/responses/). The boot-time install hook is a
// no-op in production binaries.
installNetLog()

// Tapestry write path (Phase 2). Both calls are idempotent and gated on
// the per-instance Halflight `tapestry` flag at queue-append time, so on
// non-Halflight installs they sit dormant.
attachAppStateListener()
attachAccountSwitchListener()

SplashScreen.preventAutoHideAsync()

function onAppStateChange(status: AppStateStatus) {
  // React Query already supports in web browser refetch on window focus by default
  if (Platform.OS !== 'web') {
    focusManager.setFocused(status === 'active')
  }
}

function RootLayout() {
  useOnlineManager()
  useAppState(onAppStateChange)

  const [loaded, error] = useFonts({
    ...Feather.font,
    Inter_400Regular,
    Inter_500Medium,
    Inter_600SemiBold,
    Inter_700Bold,
    Inter_800ExtraBold,
    // Tapestry weave row needs a tabular monospace so consecutive minutes
    // line up on the column grid. Loaded eagerly — the existing `useFonts`
    // gate blocks render until everything resolves, so first paint of the
    // tapestry screen never sees Inter as a fallback.
    JetBrainsMono_400Regular,
  })

  useEffect(() => {
    if (error) throw error
  }, [error])

  useEffect(() => {
    if (loaded) {
      SplashScreen.hideAsync()
    }
  }, [loaded])

  if (!loaded) return null

  return <RootLayoutNav />
}

export default function RootLayoutWithContext() {
  const router = useRouter()
  return (
    <ShareIntentProvider
      options={{
        debug: false,
        resetOnBackground: true,
        onResetShareIntent: () =>
          router.replace({
            pathname: '/(auth)/(tabs)/home',
          }),
      }}
    >
      <RootLayout />
    </ShareIntentProvider>
  )
}

// Mirror the polled notification unread count to the OS app-icon badge.
// iOS respects absolute values via setBadgeCountAsync(n); Android behavior
// is launcher-dependent but benign when the count is zero. Mounted inside
// the auth guard so it only runs while a session is live.
function AppBadgeSync() {
  const { data: unreadCount = 0 } = useQuery({
    ...notificationQueries.unreadCount(),
    // Skip persistence so the count is always freshly derived on launch.
    persister: undefined,
  })
  useEffect(() => {
    Notifications.setBadgeCountAsync(unreadCount).catch(() => {
      // Best-effort: some Android launchers reject this call outright.
    })
  }, [unreadCount])
  return null
}

function AuthenticatedStack() {
  const { user, isLoading } = useAuth()

  if (isLoading) return null

  return (
    <>
      {user ? <AppBadgeSync /> : null}
      <Stack
        screenOptions={{
          headerShown: false,
          headerBackTitle: 'Back',
          animation: 'default',
        }}
      >
        <Stack.Protected guard={!!user}>
          <Stack.Screen name="(auth)/(tabs)" />
          <Stack.Screen name="(auth)/post/[id]" />
          <Stack.Screen
            name="(auth)/story/view/[pid]"
            options={{ presentation: 'fullScreenModal', animation: 'fade' }}
          />
          <Stack.Screen
            name="(auth)/story/compose"
            options={{ presentation: 'fullScreenModal', animation: 'fade' }}
          />
          {/*
            Mastodon list management surfaces (proposal §3).
            - `lists/new` (Surface D): one-shot create form. `fitToContents`
              because the form height is fixed (one input + a toggle group +
              one switch row).
            - `lists/for/[accountId]` (Surface A): per-account membership
              picker, launched from the remote-profile overflow menu. Uses a
              tall single detent so the sheet height does not jump as
              checkboxes toggle in/out of the "Member of" group.
          */}
          <Stack.Screen
            name="(auth)/settings/lists/new"
            options={{
              presentation: 'formSheet',
              sheetAllowedDetents: 'fitToContents',
              sheetGrabberVisible: true,
              sheetCornerRadius: 16,
              headerShown: false,
            }}
          />
          <Stack.Screen
            name="(auth)/settings/lists/for/[accountId]"
            options={{
              presentation: 'formSheet',
              sheetAllowedDetents: [0.9],
              sheetGrabberVisible: true,
              sheetCornerRadius: 16,
              headerShown: false,
            }}
          />
          {/*
            List Organizer (Surface E): full-screen two-column drag surface.
            No chrome at all — both columns run edge-to-edge and the screen
            applies the top safe-area inset itself so content clears the
            status bar / notch. The native back-gesture still works.
          */}
          <Stack.Screen
            name="(auth)/settings/lists/organize"
            options={{ headerShown: false }}
          />
        </Stack.Protected>

        {/*
          handleLogin and verifyEmail are reachable from BOTH auth states:
          first-time signup/sign-in (no active account) AND from Settings →
          Accounts → Add account (already authenticated). They live in the
          `(shared-auth)` group so they sit outside both guards and the route
          group itself doesn't enforce a `!user` precondition; the screens
          branch on `?addAccount=1` (handleLogin) or just navigate explicitly
          on completion.
        */}
        <Stack.Screen name="(shared-auth)/handleLogin" />
        <Stack.Screen name="(shared-auth)/verifyEmail" />

        {/*
          Pre-auth flow lives entirely under its own nested `(public)/_layout`
          stack with an explicit `initialRouteName: 'login'`. Keeping these
          screens inside one navigator (instead of declaring them as siblings
          at the root) is what fixes the Sign Up → Back → Login black-screen:
          the always-available `handleLogin` no longer competes for the
          implicit initial-route slot of the root stack.
        */}
        <Stack.Protected guard={!user}>
          <Stack.Screen name="(public)" />
        </Stack.Protected>
      </Stack>
    </>
  )
}

// Per-account accent variant. Subscribes to AccountStore so the inner
// `<Theme name="accent${n}">` re-renders the moment the active account flips
// (or the user picks a new variant from the swatch picker). The active-
// account snapshot is already cached at `accountStore.ts:_activeAccountSnapshot`
// so this read is O(1) per emit. Pre-auth and post-sign-out-of-all the hook
// returns 1 (electric blue) — same hex as the base accent so login chrome
// looks identical to today.
function useActiveAccentVariant(): AccentVariantId {
  return useSyncExternalStore(
    AccountStore.subscribe,
    () => AccountStore.activeAccount()?.accentVariant ?? 1,
    () => 1
  )
}

function ActiveAccentTheme({ children }: { children: ReactNode }) {
  const v = useActiveAccentVariant()
  // The 16 `light_accent*` / `dark_accent*` sub-themes are registered via the
  // dynamic `Object.fromEntries` builder in tamagui.config.ts, so the inferred
  // `ThemeName` union doesn't pick up the variant keys statically. The cast is
  // sound — every variant id has a corresponding registered theme by §2.1's
  // structural iteration.
  return <Theme name={`accent${v}` as ThemeName}>{children}</Theme>
}

// Android-only. Mirrors the active Tamagui theme into the system navigation
// bar's button glyphs and the root window background so the OS chrome reads
// as one continuous surface with the page. Mounted inside `<Theme>` so
// `useTheme()` resolves to the correct light/dark `$background` token.
//
// Why two libraries: per the official expo-navigation-bar v55 docs, the
// background-color setters are deprecated and no-op under Android edge-to-
// edge enforcement. With `enforceContrast: false` set in the config plugin,
// the bar renders fully transparent and `expo-system-ui`'s root window
// background shows through. `setStyle` then controls only the button glyph
// colour. iOS short-circuits before either call.
function AndroidSystemChrome({ colorScheme }: { colorScheme: 'light' | 'dark' }) {
  const theme = useTheme()
  const background = theme.background?.val as string | undefined
  useEffect(() => {
    if (Platform.OS !== 'android') return
    // NavigationBarStyle vocabulary (per the docs): `'light'` = light bar
    // with dark content, `'dark'` = dark bar with light content. So our
    // light theme maps to `'light'` (dark glyphs on the light $background)
    // and our dark theme to `'dark'` (light glyphs on the dark $background).
    NavigationBar.setStyle(colorScheme === 'dark' ? 'dark' : 'light')
    if (background) {
      // Fire-and-forget; the next render's effect will re-apply if needed.
      void SystemUI.setBackgroundColorAsync(background)
    }
  }, [colorScheme, background])
  return null
}

function NavigationThemeProvider({
  colorScheme,
  children,
}: {
  colorScheme: string
  children: React.ReactNode
}) {
  const theme = useTheme()
  const base = colorScheme === 'dark' ? DarkTheme : DefaultTheme
  const background = theme.background?.val ?? base.colors.background
  const navTheme = {
    ...base,
    colors: {
      ...base.colors,
      background,
      card: background,
    },
  }
  return <ThemeProvider value={navTheme}>{children}</ThemeProvider>
}

// Phase 5 forward-compat (§9.3 / §9.4). Push payloads from Pixelfed should
// carry enough information for the client to route a tap to the *recipient*
// local account (so a notification for account B does not surface inside
// account A's session after a tap). Pixelfed currently emits `account_id`
// as the *actor* (the person who liked / followed / commented), so we
// deliberately do not reuse that field for the recipient; a server-side
// change must add a new pair:
//
//   - `data.instance`              — hostname the push originated from
//   - `data.recipient_account_id`  — the recipient's user_id on that
//                                    instance (not the actor's)
//
// Until that lands server-side, `resolveRecipientAccount` returns null and
// the handler falls back to today's behaviour: route under whichever
// account is currently active, no switch. Once the server lands the new
// fields, taps from background pushes silently switch the active account
// before routing.
function resolveRecipientAccount(data: Record<string, unknown>): string | null {
  const instance = typeof data.instance === 'string' ? data.instance : null
  const recipientUserId =
    typeof data.recipient_account_id === 'string' ? data.recipient_account_id : null
  if (!instance || !recipientUserId) return null
  return (
    AccountStore.findByInstanceAndUserId(instance, recipientUserId)?.accountId ?? null
  )
}

async function handleNotificationData(
  data: Record<string, unknown>,
  router: ReturnType<typeof useRouter>
) {
  // If the payload carries enough metadata to identify a known local
  // account (Phase 5, §9.3), switch active to it before routing so the
  // destination screen mounts under that account's context and the
  // account-scoped query keys load the correct data. Skipped silently
  // when the recipient is unknown locally (account was removed) or the
  // server hasn't started emitting the recipient/instance fields yet.
  // Phase 6 will surface the switch via a loading overlay; for now the
  // re-render is instantaneous from the user's perspective.
  const recipientId = resolveRecipientAccount(data)
  if (recipientId && recipientId !== AccountStore.activeId()) {
    AccountStore.setActive(recipientId)
  }

  // Mastodon Pass Phase 4 §5.2 — Mastodon's RFC 8030 push payload carries
  // an opaque `notification_id` rather than the unrolled
  // `(type, status_id, account_id)` triple Pixelfed emits. Dereference it
  // against the now-active Mastodon account so the tap can route. The
  // refetch runs after `setActive` so `mastodonActiveContext()` resolves
  // to the recipient account, not whichever account was active before.
  const activeAfterSwitch = AccountStore.activeAccount()
  if (
    activeAfterSwitch?.type === 'mastodon' &&
    typeof data.notification_id === 'string' &&
    data.type == null &&
    data.status_id == null &&
    data.account_id == null
  ) {
    const fresh = await getMastodonNotification(String(data.notification_id))
    if (fresh) {
      data = {
        ...data,
        type: fresh.type,
        status_id: fresh.status?.id,
        account_id: fresh.account?.id,
      }
    }
  }

  switch (data.type) {
    case 'favourite':
    case 'reblog':
    case 'mention':
    case 'quote':
    case 'quoted_update':
    case 'update':
    case 'poll':
    case 'status':
      if (data.status_id) router.push(`/post/${data.status_id}`)
      break
    case 'follow':
    case 'follow_request':
      if (data.account_id) router.push(`/profile/${data.account_id}`)
      break
    case 'direct':
      if (data.account_id) router.push(`/chats/conversation/${data.account_id}`)
      break
  }
}

// Reserve enough vertical space above the floating tab bar + per-screen
// toolbar so toasts never render underneath them. The tab bar metric lives
// in src/components/menu/tabBarMetrics so the two stay in sync.
const TOAST_BOTTOM_RESERVE = TAB_BAR_HEIGHT + 16

function RootLayoutNav() {
  const colorScheme = useStoredTheme()
  const router = useRouter()
  const insets = useSafeAreaInsets()
  const notificationListenerRef = useRef<Notifications.EventSubscription>(null)

  // Handle notification taps while app is running
  useEffect(() => {
    notificationListenerRef.current =
      Notifications.addNotificationResponseReceivedListener((response) => {
        const data = response.notification.request.content.data
        if (data) handleNotificationData(data, router)
      })
    return () => notificationListenerRef.current?.remove()
  }, [router])

  // Handle cold-start notification tap
  const lastNotificationResponse = Notifications.useLastNotificationResponse()
  useEffect(() => {
    if (lastNotificationResponse !== undefined && lastNotificationResponse !== null) {
      const data = lastNotificationResponse.notification.request.content.data
      if (data) handleNotificationData(data, router)
    }
  }, [lastNotificationResponse, router])

  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <SafeAreaProvider initialMetrics={initialWindowMetrics}>
        <QueryClientProvider client={queryClient}>
          <AuthProvider>
            <TamaguiProvider config={config} defaultTheme="light">
              <Theme name={colorScheme}>
                <ActiveAccentTheme>
                  <NavigationThemeProvider colorScheme={colorScheme}>
                    <ToastProvider native={false}>
                      <BottomSheetModalProvider>
                        <ImageViewerProvider>
                          <StatusBar
                            style={colorScheme === 'dark' ? 'light' : 'dark'}
                            animated
                          />
                          <AndroidSystemChrome colorScheme={colorScheme} />
                          <ToastViewport
                            // Pin to the bottom edge above the tab bar +
                            // system safe area and span the full width
                            // (minus display-cutout insets). NOTHING
                            // ELSE — no `alignItems`, no padding. Centering
                            // is owned entirely by the toast itself via
                            // `marginHorizontal: 'auto'` (the default
                            // variant of @tamagui/toast's ToastImplFrame)
                            // composed with the `<Animated.View
                            // style={{ margin: 'auto' }}>` wrapper Tamagui
                            // injects between the viewport and the toast
                            // frame at ToastImpl.tsx:374. Adding a second
                            // centering signal here (alignItems, padding,
                            // alignSelf, etc.) makes Yoga's auto-margin
                            // distribution undefined and pins the toast
                            // to one edge. One centering mechanism, never
                            // two.
                            bottom={TOAST_BOTTOM_RESERVE + insets.bottom}
                            left={insets.left}
                            right={insets.right}
                          />
                          <GlobalToast />
                          <AuthenticatedStack />
                          <MenuBarToolbar />
                          <ImageViewerOverlay />
                          <AccountSwitchOverlay />
                          <PermissionDisclosure />
                          <WatchHandoffBridge />
                          <DevSeedListener />
                        </ImageViewerProvider>
                      </BottomSheetModalProvider>
                    </ToastProvider>
                  </NavigationThemeProvider>
                </ActiveAccentTheme>
              </Theme>
            </TamaguiProvider>
          </AuthProvider>
        </QueryClientProvider>
      </SafeAreaProvider>
    </GestureHandlerRootView>
  )
}
