import { Feather } from '@expo/vector-icons'
import { useRecyclingState } from '@shopify/flash-list'
import { Image } from 'expo-image'
import { Link, router } from 'expo-router'
import React, { useCallback, useEffect, useMemo } from 'react'
import { Platform, Pressable, Share } from 'react-native'
import { Gesture, GestureDetector, type PanGesture } from 'react-native-gesture-handler'
import { useMMKVBoolean } from 'react-native-mmkv'
import { runOnJS, type SharedValue, useSharedValue } from 'react-native-reanimated'
import Carousel, { Pagination } from 'react-native-reanimated-carousel'
import { onMentionPressMethod } from 'src/components/common/AutolinkText'
import { PressableOpacity } from 'src/components/common/PressableOpacity'
import StatusContent from 'src/components/common/StatusContent'
import { useLikeMutation } from 'src/hooks/mutations/useLikeMutation'
import { useConfirmDialog } from 'src/hooks/useConfirmDialog'
import { useImageViewer } from 'src/hooks/useImageViewer'
import { useStatusKind } from 'src/hooks/useStatusKind'
import { useWindowWidth } from 'src/hooks/useWindowWidth'
import type {
  CustomEmoji,
  LoginUserResponse,
  MediaAttachment,
  Status,
  Tag,
  Timestamp,
  Visibility,
} from 'src/lib/api-types'
import { synthesizeMobilizonEventFromMastodonStatus } from 'src/lib/events'
import { useFeature } from 'src/lib/features'
import { haptic } from 'src/lib/haptics'
import { ICON } from 'src/lib/icons'
import { isVideoPost, toImageUri, toVideoSource } from 'src/lib/media-source'
import { useAuth } from 'src/state/AuthProvider'
import { appStorage, uiKeys } from 'src/state/storage'
import {
  formatTimestamp,
  getReplyCount,
  preprocessStatusContent,
  safeOpenStatusOriginal,
  timeAgo,
} from 'src/utils'
import {
  Menu,
  Separator,
  Text,
  useTheme,
  useThemeName,
  View,
  XStack,
  YStack,
  ZStack,
} from 'tamagui'
import ArticleCard from './ArticleCard'
import EventCard from './EventCard'
import LinkPreviewCard from './LinkPreviewCard'
import PostActions from './PostActions'
import PostHeader from './PostHeader'
import QuotedStatus from './QuotedStatus'
import VideoPlayer from './VideoPlayer'

// expo-image's `recyclingKey` + `transition` combination is broken on iOS
// (expo/expo#22516). Dropping the cross-fade on the feed hot path also saves
// frames on scroll-recycle.
const IMAGE_TRANSITION_MS = Platform.OS === 'ios' ? 0 : 300

const BorderlessSection = React.memo(({ children }: React.PropsWithChildren) => {
  const theme = useTheme()
  return (
    <View px="$3" backgroundColor={theme.background.val}>
      {children}
    </View>
  )
})

interface PostMediaProps {
  media: Array<MediaAttachment>
  post: Status
  isReblog?: boolean
}

const PostMedia = React.memo(({ media, post, isReblog }: PostMediaProps) => {
  const mediaUrl = toImageUri(media[0])
  // useRecyclingState auto-resets when the post id changes, so a previously
  // revealed sensitive post doesn't stay revealed when the cell recycles to
  // a different sensitive post.
  const [showSensitive, setSensitive] = useRecyclingState(false, [post.id])
  const width = useWindowWidth()
  const theme = useTheme()
  const [forceSensitiveFlag] = useMMKVBoolean(uiKeys.forceSensitive, appStorage)
  const forceSensitive = forceSensitiveFlag === true
  const height = media[0].meta?.original?.width
    ? width * (media[0].meta?.original?.height / media[0].meta?.original.width)
    : 430

  if (!forceSensitive && post.sensitive && !showSensitive) {
    return (
      <ZStack w={width} h={height}>
        <YStack justifyContent="center" alignItems="center" flexGrow={1}>
          <YStack
            justifyContent="center"
            alignItems="center"
            flexGrow={1}
            m="$3"
            gap="$5"
          >
            <Feather name="eye-off" size={ICON.xl} color="white" />
            <Text fontSize="$5" color="white" allowFontScaling={false}>
              This post contains sensitive or mature content
            </Text>
          </YStack>
          <YStack w={width} flexShrink={1}>
            <Separator borderColor={theme.borderColor.val} />
            <PressableOpacity onPress={() => setSensitive(true)}>
              <View p="$4" justifyContent="center" alignItems="center">
                <Text
                  fontSize="$4"
                  color="white"
                  fontWeight={'bold'}
                  allowFontScaling={false}
                >
                  Tap to view
                </Text>
              </View>
            </PressableOpacity>
            <Separator borderColor={theme.borderColor.val} />
          </YStack>
        </YStack>
      </ZStack>
    )
  }

  if (isVideoPost(post)) {
    return (
      <VideoPlayer
        source={toVideoSource(media[0])}
        height={height}
        videoId={post.id}
        previewUrl={media[0].preview_url}
        blurhash={media[0].blurhash}
      />
    )
  }

  return (
    <View
      flex={1}
      borderBottomWidth={isReblog ? 0 : 1}
      borderBottomColor="$gray5"
      zIndex={100}
    >
      <Image
        placeholder={{ blurhash: media[0]?.blurhash || '' }}
        placeholderContentFit="cover"
        source={{ uri: mediaUrl }}
        style={{ width, height, backgroundColor: '#000' }}
        recyclingKey={`${post.id}:0`}
        cachePolicy="memory-disk"
        priority="high"
        transition={IMAGE_TRANSITION_MS}
        contentFit="contain"
      />
    </View>
  )
})

const calculateHeight = (item: MediaAttachment, width: number) => {
  if (item.meta?.original?.width) {
    return width * (item.meta.original.height / item.meta.original.width)
  }
  return 500
}

// Stable wrapper that the carousel's renderItem delegates to. Memoizing on
// (item.id, item.url, width, height) keeps the ZoomableImage subtree from
// being reconstructed when an unrelated prop on the parent post changes.
// item.id is included because two different posts can theoretically share
// the same first-image URL (cross-posts, re-uploads).
const AlbumSlide = React.memo(
  ({
    item,
    width,
    height,
    postId,
    index,
  }: {
    item: MediaAttachment
    width: number
    height: number
    postId: string
    index: number
  }) => (
    <Image
      placeholder={{ blurhash: item?.blurhash || '' }}
      placeholderContentFit="cover"
      source={{ uri: toImageUri(item) }}
      style={{ width, height, backgroundColor: '#000' }}
      recyclingKey={`${postId}:${index}`}
      cachePolicy="memory-disk"
      priority="high"
      transition={IMAGE_TRANSITION_MS}
      contentFit="contain"
    />
  ),
  (prev, next) =>
    prev.item.id === next.item.id &&
    prev.item.url === next.item.url &&
    prev.width === next.width &&
    prev.height === next.height &&
    prev.postId === next.postId &&
    prev.index === next.index
)

interface PostAlbumMediaProps {
  media: Array<MediaAttachment>
  post: Status
  progress: SharedValue<number>
}

const PostAlbumMedia = React.memo(({ media, post, progress }: PostAlbumMediaProps) => {
  const [showSensitive, setSensitive] = useRecyclingState(false, [post.id])
  const width = useWindowWidth()
  const height = useMemo(
    () =>
      media.reduce((max, item) => {
        const h = calculateHeight(item, width)
        return h > max ? h : max
      }, 0),
    [media, width]
  )
  const theme = useTheme()
  const themeName = useThemeName()
  const dotColor = themeName == 'dark' ? 'rgba(255, 255, 255, 0.20)' : 'rgba(0,0,0,0.11)'
  const mediaList = useMemo(
    () => post.media_attachments.slice(0, 25),
    [post.media_attachments]
  )

  // FlashList recycling: when this cell rebinds to a new post the carousel's
  // progress shared value still holds the previous post's position, which
  // flashes a stale pagination dot on the first frame. Snap back to zero.
  useEffect(() => {
    progress.value = 0
  }, [post.id, progress])

  // Warm the decode cache for the next 1–2 album slides so the first
  // horizontal swipe doesn't stall on image decode. `windowSize={2}` already
  // mounts [current, current+1] but its decode races the swipe on slower
  // devices. Bounded to 2 URLs to cap memory (expo/expo#40544 — memory-disk
  // doesn't purge). Matches the feed's `cachePolicy="memory-disk"` so the
  // prefetched entry is the same entry the carousel render will hit.
  useEffect(() => {
    const urls = mediaList.slice(1, 3).map((m) => toImageUri(m))
    if (urls.length === 0) return
    Image.prefetch(urls, 'memory-disk').catch(() => {})
  }, [mediaList])

  const renderCarouselItem = useCallback(
    ({ index }: { index: number }) => (
      <AlbumSlide
        item={mediaList[index]}
        width={width}
        height={height}
        postId={post.id}
        index={index}
      />
    ),
    [mediaList, width, height, post.id]
  )

  // Keep the pan on the UI thread and hand off to the parent FlashList's
  // vertical scroll after 8dp of vertical travel. `.runOnJS(true)` previously
  // dispatched every pan update to the JS thread — catastrophic for swipe feel.
  const configurePan = useCallback(
    (g: PanGesture) => g.activeOffsetX([-12, 12]).failOffsetY([-8, 8]),
    []
  )

  if (post.sensitive && !showSensitive) {
    return (
      <ZStack w={width} h={height}>
        <Image
          placeholder={{ blurhash: media[0]?.blurhash || '' }}
          placeholderContentFit="cover"
          contentFit="cover"
          recyclingKey={post.id}
          style={{
            flex: 1,
            width: width,
            height: height,
          }}
        />
        <YStack justifyContent="center" alignItems="center" flexGrow={1}>
          <YStack justifyContent="center" alignItems="center" flexGrow={1} gap="$3">
            <Feather name="eye-off" size={ICON.xl} color="white" />
            <Text fontSize="$7" color="white">
              This post contains sensitive or mature content
            </Text>
          </YStack>
          <YStack w={width} flexShrink={1}>
            <Separator borderColor={theme.borderColor.val} />

            <PressableOpacity onPress={() => setSensitive(true)}>
              <View p="$4" justifyContent="center" alignItems="center">
                <Text
                  fontSize="$4"
                  color="white"
                  fontWeight={'bold'}
                  allowFontScaling={false}
                >
                  Tap to view
                </Text>
              </View>
            </PressableOpacity>
            <Separator borderColor={theme.borderColor.val} />
          </YStack>
        </YStack>
      </ZStack>
    )
  }

  return (
    <YStack zIndex={1}>
      <Carousel
        onConfigurePanGesture={configurePan}
        width={width}
        height={height}
        vertical={false}
        loop={false}
        windowSize={2}
        onProgressChange={progress}
        data={mediaList}
        renderItem={renderCarouselItem}
      />
      <Pagination.Basic
        progress={progress}
        data={mediaList}
        dotStyle={{ backgroundColor: dotColor, borderRadius: 50 }}
        activeDotStyle={{
          backgroundColor: theme.interactiveActive.val,
          borderRadius: 50,
        }}
        containerStyle={{
          gap: 2,
          position: 'absolute',
          bottom: 0,
          marginBottom: -10,
          zIndex: 10,
        }}
        size={7}
      />
    </YStack>
  )
})

interface PostCaptionProps {
  postId: string
  caption: string
  commentsCount: number
  createdAt: Timestamp
  tags: Array<Tag>
  visibility: Visibility
  onOpenComments: () => void
  onHashtagPress: (tag: string) => void
  onMentionPress: (username: string, isLocalUsername: boolean) => void
  onUsernamePress: () => void
  disableReadMore: boolean
  editedAt: Timestamp | null
  isLikeFeed: boolean
  likedAt: Timestamp | null
  isPermalink: boolean
  emojis?: Array<CustomEmoji>
  /** When true, the caption body has already been hoisted above the media
   * (Mastodon caption-above-media layout), so PostCaption renders only the
   * meta line and the comments link. */
  bodyAboveMedia?: boolean
}

const PostCaption = React.memo(
  ({
    postId,
    caption,
    commentsCount,
    createdAt,
    tags: _tags,
    visibility,
    onOpenComments,
    onHashtagPress,
    onMentionPress,
    onUsernamePress,
    disableReadMore,
    editedAt,
    isLikeFeed,
    likedAt,
    isPermalink,
    emojis,
    bodyAboveMedia,
  }: PostCaptionProps) => {
    const createdAtLabel = formatTimestamp(createdAt)
    const theme = useTheme()
    return (
      <BorderlessSection>
        <YStack gap="$3" pt="$1" pb="$5" px="$2">
          {bodyAboveMedia ? null : (
            <View pr="$3">
              <StatusContent
                html={caption}
                emojis={emojis}
                onHashtagPress={onHashtagPress}
                onMentionPress={onMentionPress}
                onUsernamePress={onUsernamePress}
                disableReadMore={disableReadMore}
                thresholdChars={420}
                trailingHashtagsAsPills
              />
            </View>
          )}
          {isPermalink && commentsCount ? (
            <Pressable onPress={() => onOpenComments()}>
              <Text color={theme.colorSecondary.val} fontSize="$3">
                {commentsCount > 1
                  ? `View all ${commentsCount} comments`
                  : 'View 1 comment'}
              </Text>
            </Pressable>
          ) : null}

          {isPermalink ? (
            <XStack justifyContent="flex-start" alignItems="center" gap="$3">
              {visibility == 'public' ? (
                <XStack alignItems="center" gap="$2">
                  <Text color={theme.colorSecondary.val} fontSize="$3">
                    Public
                  </Text>
                </XStack>
              ) : null}
              {visibility == 'unlisted' ? (
                <XStack alignItems="center" gap="$2">
                  <Text color={theme.colorSecondary.val} fontSize="$3">
                    Unlisted
                  </Text>
                </XStack>
              ) : null}
              {visibility == 'private' ? (
                <XStack alignItems="center" gap="$2">
                  <Feather name="lock" color={theme.colorSecondary.val} />
                  <Text color={theme.colorSecondary.val} fontSize="$3">
                    Followers only
                  </Text>
                </XStack>
              ) : null}
              <Link href={`/post/${postId}`} asChild>
                <XStack alignItems="center" gap="$2">
                  <Feather name="clock" color={theme.colorTertiary.val} />
                  <Text color={theme.colorSecondary.val} fontSize="$3">
                    {createdAtLabel}
                  </Text>
                </XStack>
              </Link>
              {editedAt ? (
                <Link href={`/post/history/${postId}`} asChild>
                  <XStack alignItems="center" gap="$2">
                    <Feather name="edit" color={theme.colorSecondary.val} />
                    <Text color={theme.colorSecondary.val} fontSize="$3">
                      Last Edited {timeAgo(editedAt)}
                    </Text>
                  </XStack>
                </Link>
              ) : null}
              {isLikeFeed && likedAt ? (
                <XStack alignItems="center" gap="$2">
                  <Feather name="heart" color={theme.colorTertiary.val} />
                  <Text color={theme.colorSecondary.val} fontSize="$3">
                    Liked {timeAgo(likedAt)} ago
                  </Text>
                </XStack>
              ) : null}
            </XStack>
          ) : null}
        </YStack>
      </BorderlessSection>
    )
  }
)

interface FeedPostProps {
  post: Status
  user: LoginUserResponse
  onOpenComments: (id: string) => void
  onDeletePost: (id: string) => void
  disableReadMore?: boolean
  isPermalink?: boolean
  isLikeFeed?: boolean
  onShare: (id: string) => void
  handleLikeProfileId?: boolean
  isReblog?: boolean
  booster?: Status['account']
}

interface TextPostBodyProps {
  post: Status
  user: LoginUserResponse
  isPermalink: boolean
  isLikeFeed: boolean
  booster?: Status['account']
  menuContent: React.ReactNode
  disableReadMore: boolean
  captionHtml: string
  quotedStatus: React.ReactNode
  progress: SharedValue<number>
  isLikePending: boolean
  showAltText: boolean
  handleLike: () => void
  onOpenComments: () => void
  onShare: () => void
  onHashtagPress: (tag: string) => void
  onMentionPress: (mention: string, isLocal: boolean) => void
  onUsernamePress: () => void
}

// Renders a text-only status using the shared PostHeader + caption body +
// PostActions stack. Previously this was a hand-rolled layout with its own
// inline header and compact action row — which is how the "username
// instead of display_name", "no custom emoji in display name", and
// "smaller comment icon" bugs crept in. Keeping text and media posts on
// the same scaffolding is the fix.
const TextPostBody = React.memo(function TextPostBody({
  post,
  user: _user,
  isPermalink,
  isLikeFeed,
  booster,
  menuContent,
  disableReadMore,
  captionHtml,
  quotedStatus,
  progress,
  isLikePending,
  showAltText,
  handleLike,
  onOpenComments,
  onShare,
  onHashtagPress,
  onMentionPress,
  onUsernamePress,
}: TextPostBodyProps) {
  const theme = useTheme()
  const createdAtLabel = isPermalink ? formatTimestamp(post.created_at) : null

  return (
    <View backgroundColor={theme.background.val}>
      {post.in_reply_to_id && !isPermalink ? (
        <View px="$3" pt="$2">
          <XStack>
            <Text color={theme.colorSecondary.val}>In reply to this </Text>
            <Link href={`/post/${post.in_reply_to_id}`} asChild>
              <Pressable>
                <Text color={theme.interactiveActive.val} fontWeight="bold">
                  post
                </Text>
              </Pressable>
            </Link>
          </XStack>
        </View>
      ) : null}

      <PostHeader
        variant="text"
        avatar={post.account?.avatar}
        username={post.account?.acct}
        displayName={post.account?.display_name}
        userId={post.account?.id}
        menuContent={menuContent}
        booster={booster}
        emojis={post.account?.emojis}
        timeAgo={createdAtLabel}
      />

      <View px="$3" backgroundColor={theme.background.val}>
        <YStack pt="$1" pb="$3" px="$2" gap="$3">
          <View pr="$3">
            <StatusContent
              html={captionHtml}
              emojis={post.emojis}
              onHashtagPress={onHashtagPress}
              onMentionPress={onMentionPress}
              onUsernamePress={onUsernamePress}
              disableReadMore={disableReadMore}
              thresholdChars={280}
              trailingHashtagsAsPills
            />
          </View>
          {quotedStatus}
          <LinkPreviewCard card={post.card} />
        </YStack>
      </View>

      <PostActions
        variant="text"
        hasLiked={post.favourited as boolean}
        hasShared={post?.reblogged === true}
        hasBookmarked={post?.bookmarked === true}
        isLikeFeed={isLikeFeed}
        post={post}
        progress={progress}
        isLikePending={isLikePending}
        likesCount={post?.favourites_count}
        likedBy={post?.liked_by}
        sharesCount={post?.reblogs_count}
        showAltText={showAltText}
        commentsCount={getReplyCount(post)}
        handleLike={handleLike}
        onOpenComments={onOpenComments}
        onShare={onShare}
      />
    </View>
  )
})

const FeedPost = React.memo(
  function FeedPost({
    post,
    user,
    onOpenComments,
    onDeletePost,
    disableReadMore = false,
    isPermalink = false,
    isLikeFeed = false,
    onShare,
    handleLikeProfileId = false,
    isReblog = false,
    booster,
  }: FeedPostProps) {
    const { handleLike, isLikePending } = useLikeMutation()
    const progress = useSharedValue(0)
    const width = useWindowWidth()
    const { open: openImageViewer } = useImageViewer()
    const [hideCaptionsFlag] = useMMKVBoolean(uiKeys.hideCaptions, appStorage)
    const [showAltTextFlag] = useMMKVBoolean(uiKeys.showAltText, appStorage)
    const hideCaptions = hideCaptionsFlag === true
    const showAltText = showAltTextFlag === true
    const theme = useTheme()
    const { confirm, dialog: confirmDialog } = useConfirmDialog()
    const quotePostsEnabled = useFeature('quote_posts')
    const eventIngestEnabled = useFeature('event_ingest')
    const articleIngestEnabled = useFeature('article_ingest')
    const hasQuote = quotePostsEnabled && !!post.quote?.state
    const quoteAccepted = hasQuote && post.quote?.state === 'accepted'
    const captionContent = quoteAccepted
      ? preprocessStatusContent(post.content ?? '')
      : post.content
    const quoteNode = hasQuote ? <QuotedStatus quote={post.quote!} /> : null
    // Mastodon convention is text-above-media: a substantive paragraph with an
    // illustrative attachment, not Pixelfed's Instagram-style caption-below.
    // We branch render order at the cell level only — the caption renderer
    // (StatusContent / PostCaption) is type-agnostic and reused as-is.
    const isMastodonAccount = useAuth().user?.type === 'mastodon'
    const hasCaptionBody = !!captionContent && captionContent.trim().length > 0
    // Phase B §3.1 of the Mastodon follow-up: `useStatusKind` returns 'event'
    // on Pixelfed iff `pf_type === 'event'`, and on Mastodon iff the author's
    // host nodeinfo classifies as Mobilizon (probed lazily, cached forever).
    // The latter case has no `pf_event` sidecar, so we synthesize a minimal
    // projection from the status content + URL + first media attachment.
    const statusKind = useStatusKind(post)
    const isEventPost =
      statusKind === 'event' &&
      // Pixelfed: respect the existing `event_ingest` feature flag (matches
      // server-side behaviour). Mastodon: server-side `event_ingest` will
      // never be advertised, so the kind itself is the gate.
      ((eventIngestEnabled && !!post.pf_event) || !post.pf_event)
    const eventPost = useMemo(() => {
      if (!isEventPost) return post
      if (post.pf_event) return post
      return {
        ...post,
        pf_event: synthesizeMobilizonEventFromMastodonStatus(post),
      }
    }, [isEventPost, post])
    const isArticlePost =
      articleIngestEnabled && post.pf_type === 'article' && !!post.pf_article

    const handleLikeAction = useCallback(() => {
      const shouldLike = !post.favourited
      handleLike(
        post.id,
        shouldLike,
        handleLikeProfileId ? post.account.id.toString() : ''
      )
    }, [post.id, post.favourited, post.favourites_count, handleLike])

    const handleDoubleTap = useCallback(() => {
      // only allow liking with double tap, not unliking
      if (!post.favourited) {
        haptic.medium()
        handleLikeAction()
      }
    }, [post.favourited, handleLikeAction])

    // Canonical RNGH v2 single-tap + double-tap pattern. Both handlers use
    // `onEnd((_, success) => { if (success) … })` so only the winning
    // gesture's closure fires — Exclusive resolves by making the loser's
    // `onEnd` run with success=false. Using `onStart` here is ambiguous
    // under Exclusive and doesn't expose the success flag, which bit us in
    // the first attempt.
    const doubleTap = useMemo(
      () =>
        Gesture.Tap()
          .maxDuration(250)
          .numberOfTaps(2)
          .onEnd((_e, success) => {
            'worklet'
            if (success) runOnJS(handleDoubleTap)()
          }),
      [handleDoubleTap]
    )

    // Single tap opens the app-wide image viewer overlay for the currently-
    // visible media item. For albums, read the active slide index from the
    // carousel's `progress` SharedValue — at the moment singleTap fires, the
    // finger has lifted and `progress` has settled to an integer slot.
    const openCurrentImage = useCallback(() => {
      const idx = post.media_attachments.length > 1 ? Math.round(progress.value) : 0
      const m = post.media_attachments[idx]
      if (!m?.url || isVideoPost(post)) return
      openImageViewer({
        uri: toImageUri(m),
        blurhash: m.blurhash ?? undefined,
        width: m.meta?.original?.width,
        height: m.meta?.original?.height,
      })
    }, [post, progress, openImageViewer])

    const singleTap = useMemo(
      () =>
        Gesture.Tap()
          .numberOfTaps(1)
          .onEnd((_e, success) => {
            'worklet'
            if (success) runOnJS(openCurrentImage)()
          }),
      [openCurrentImage]
    )

    // Exclusive gives doubleTap priority — singleTap fires ~500ms after
    // finger release (RNGH default `maxDelay`) if no second tap arrives.
    // `requireExternalGestureToFail` is the wrong tool here: it's for
    // coordinating gestures across *different* GestureDetectors.
    const tapComposition = useMemo(
      () => Gesture.Exclusive(doubleTap, singleTap),
      [doubleTap, singleTap]
    )

    const _onDeletePost = useCallback(
      async (id: string) => {
        const ok = await confirm({
          title: 'Delete Post',
          message: 'Are you sure you want to delete this post?',
          confirmLabel: 'Delete',
          destructive: true,
        })
        if (ok) onDeletePost(id)
      },
      [confirm, onDeletePost]
    )

    const goToPost = useCallback(() => {
      router.push(`/post/${post.id}`)
    }, [post.id])

    const goToProfile = useCallback(() => {
      router.push(`/profile/${post.account.id}`)
    }, [post.account.id])

    const goToReport = useCallback(() => {
      router.push(`/post/report/${post.id}`)
    }, [post.id])

    const openInBrowser = useCallback(async () => {
      await safeOpenStatusOriginal(post)
    }, [post.url, post.uri])

    const onGotoHashtag = useCallback((tag: string) => {
      router.push(`/hashtag/${tag}`)
    }, [])

    const onGotoMention = useCallback((tag: string) => {
      router.push(`/profile/0?byUsername=${tag}`)
    }, [])

    const onGotoAbout = useCallback(() => {
      router.push(`/profile/about/${post.account.id}`)
    }, [post.account.id])

    const _onEditPost = useCallback((id: string) => {
      router.push(`/post/edit/${id}`)
    }, [])

    const onGotoShare = useCallback(async () => {
      try {
        await Share.share({ message: post.url || post.uri })
      } catch (_error) {}
    }, [post.url, post.uri])

    const onGotoQuote = useCallback(() => {
      router.push(`/camera?quotedStatusId=${post.id}`)
    }, [post.id])

    // Bind parent-provided handlers to this post once. Without this wrapping,
    // every render passes a fresh arrow-function to memo'd children (PostActions,
    // PostCaption, TextPost, EventCard) and erases the memo savings.
    const onOpenCommentsForThis = useCallback(
      () => onOpenComments(post?.id),
      [onOpenComments, post?.id]
    )
    const onShareForThis = useCallback(() => onShare(post?.id), [onShare, post?.id])
    const onHashtagPress = useCallback(
      (tag: string) => onGotoHashtag(tag),
      [onGotoHashtag]
    )
    const onMentionPress = useMemo(
      () => onMentionPressMethod(onGotoMention, post.account.url),
      [onGotoMention, post.account.url]
    )
    const onUsernamePress = useCallback(() => goToProfile(), [goToProfile])
    const onEditThisPost = useCallback(() => _onEditPost(post.id), [_onEditPost, post.id])
    const onDeleteThisPost = useCallback(
      () => _onDeletePost(post.id),
      [_onDeletePost, post.id]
    )

    // Rebuild only when post identity, viewer identity, or feature flags
    // toggle. Without this, every recycled render rebuilt a Menu tree with
    // ~7 Menu.Item children and zeego processed them eagerly. The theme
    // color is captured into the memo, so the icon still re-colors when the
    // user changes theme (themeName change re-renders the parent anyway).
    const postMenuContent = useMemo(
      () => (
        <Menu>
          <Menu.Trigger>
            <View px="$3" hitSlop={8}>
              <Feather
                name={Platform.OS === 'ios' ? 'more-horizontal' : 'more-vertical'}
                size={ICON.lg}
                color={theme.color.val}
              />
            </View>
          </Menu.Trigger>
          <Menu.Content>
            <Menu.Item key="share" onSelect={onGotoShare}>
              <Menu.ItemTitle>Share</Menu.ItemTitle>
              <Menu.ItemIcon ios={{ name: 'square.and.arrow.up' }} />
            </Menu.Item>
            {quotePostsEnabled ? (
              <Menu.Item key="quote" onSelect={onGotoQuote}>
                <Menu.ItemTitle>Quote</Menu.ItemTitle>
                <Menu.ItemIcon ios={{ name: 'quote.bubble' }} />
              </Menu.Item>
            ) : null}
            {!isPermalink ? (
              <Menu.Item key="view-post" onSelect={goToPost}>
                <Menu.ItemTitle>View Post</Menu.ItemTitle>
                <Menu.ItemIcon ios={{ name: 'doc.richtext' }} />
              </Menu.Item>
            ) : null}
            <Menu.Item key="view-profile" onSelect={goToProfile}>
              <Menu.ItemTitle>View Profile</Menu.ItemTitle>
              <Menu.ItemIcon ios={{ name: 'person' }} />
            </Menu.Item>
            <Menu.Item key="about" onSelect={onGotoAbout}>
              <Menu.ItemTitle>About this account</Menu.ItemTitle>
              <Menu.ItemIcon ios={{ name: 'info.circle' }} />
            </Menu.Item>
            <Menu.Item key="browser" onSelect={openInBrowser}>
              <Menu.ItemTitle>Open in browser</Menu.ItemTitle>
              <Menu.ItemIcon ios={{ name: 'safari' }} />
            </Menu.Item>
            {user && user?.id !== post?.account?.id ? (
              <Menu.Item key="report" destructive onSelect={goToReport}>
                <Menu.ItemTitle>Report</Menu.ItemTitle>
                <Menu.ItemIcon ios={{ name: 'exclamationmark.triangle' }} />
              </Menu.Item>
            ) : null}
            {user && user?.id === post?.account?.id ? (
              <Menu.Group>
                <Menu.Item key="edit" onSelect={onEditThisPost}>
                  <Menu.ItemTitle>Edit Post</Menu.ItemTitle>
                  <Menu.ItemIcon ios={{ name: 'pencil' }} />
                </Menu.Item>
                <Menu.Item key="delete" destructive onSelect={onDeleteThisPost}>
                  <Menu.ItemTitle>Delete Post</Menu.ItemTitle>
                  <Menu.ItemIcon ios={{ name: 'trash' }} />
                </Menu.Item>
              </Menu.Group>
            ) : null}
          </Menu.Content>
        </Menu>
      ),
      [
        theme.color.val,
        quotePostsEnabled,
        isPermalink,
        user,
        post?.account?.id,
        onGotoShare,
        onGotoQuote,
        goToPost,
        goToProfile,
        onGotoAbout,
        openInBrowser,
        goToReport,
        onEditThisPost,
        onDeleteThisPost,
      ]
    )

    // Boost wrapper: the outer Status carries only the booster's account and
    // empty content/media; the real post hangs off `post.reblog`. Recurse with
    // the inner status and pass the booster so its avatar overlays the author's.
    // Placed after every hook above so hook order stays stable.
    if (post.reblog) {
      return (
        <FeedPost
          post={post.reblog}
          user={user}
          onOpenComments={onOpenComments}
          onDeletePost={onDeletePost}
          onShare={onShare}
          disableReadMore={disableReadMore}
          isPermalink={isPermalink}
          isLikeFeed={isLikeFeed}
          handleLikeProfileId={handleLikeProfileId}
          isReblog={true}
          booster={post.account}
        />
      )
    }

    if (isEventPost) {
      return (
        <View flex={1} pb="$4" style={{ width }}>
          <EventCard
            post={eventPost}
            actions={{
              hasLiked: post.favourited as boolean,
              hasShared: post?.reblogged === true,
              hasBookmarked: post?.bookmarked === true,
              isLikePending,
              isLikeFeed,
              likesCount: post?.favourites_count,
              sharesCount: post?.reblogs_count,
              commentsCount: getReplyCount(post),
              progress,
              handleLike: handleLikeAction,
              onOpenComments: onOpenCommentsForThis,
              onShare: onShareForThis,
            }}
          />
          {quoteNode}
          {confirmDialog}
        </View>
      )
    }

    if (isArticlePost) {
      return (
        <View flex={1} pb="$4" style={{ width }}>
          <PostHeader
            avatar={post.account?.avatar}
            username={post.account?.acct}
            displayName={post.account?.display_name}
            userId={post.account?.id}
            menuContent={postMenuContent}
            booster={booster}
            emojis={post.account?.emojis}
          />
          <ArticleCard post={post} />
          {quoteNode}
          {!hideCaptions || isPermalink ? (
            <>
              <PostActions
                hasLiked={post.favourited as boolean}
                hasShared={post?.reblogged === true}
                hasBookmarked={post?.bookmarked === true}
                isLikeFeed={isLikeFeed}
                post={post}
                progress={progress}
                isLikePending={isLikePending}
                likesCount={post?.favourites_count}
                likedBy={post?.liked_by}
                sharesCount={post?.reblogs_count}
                showAltText={showAltText}
                commentsCount={getReplyCount(post)}
                handleLike={handleLikeAction}
                onOpenComments={onOpenCommentsForThis}
                onShare={onShareForThis}
              />

              <PostCaption
                postId={post.id}
                caption={captionContent}
                commentsCount={getReplyCount(post)}
                createdAt={post.created_at}
                tags={post.tags}
                visibility={post.visibility}
                disableReadMore={disableReadMore}
                onOpenComments={onOpenCommentsForThis}
                onHashtagPress={onHashtagPress}
                onMentionPress={onMentionPress}
                onUsernamePress={onUsernamePress}
                editedAt={post.edited_at}
                isLikeFeed={isLikeFeed}
                likedAt={post.liked_at}
                isPermalink={isPermalink}
                emojis={post.emojis}
              />
            </>
          ) : null}
          {confirmDialog}
        </View>
      )
    }

    const captionAboveMedia = isMastodonAccount && hasCaptionBody
    return (
      <View flex={1} pb="$4" style={{ width }}>
        {post.media_attachments?.length > 0 ? (
          <>
            <PostHeader
              avatar={post.account?.avatar}
              username={post.account?.acct}
              displayName={post.account?.display_name}
              userId={post.account?.id}
              menuContent={postMenuContent}
              booster={booster}
              emojis={post.account?.emojis}
            />

            {captionAboveMedia ? (
              <BorderlessSection>
                <YStack pt="$1" pb="$3" px="$2">
                  <View pr="$3">
                    <StatusContent
                      html={captionContent ?? ''}
                      emojis={post.emojis}
                      onHashtagPress={onHashtagPress}
                      onMentionPress={onMentionPress}
                      onUsernamePress={onUsernamePress}
                      disableReadMore={disableReadMore}
                      thresholdChars={420}
                      trailingHashtagsAsPills
                    />
                  </View>
                </YStack>
              </BorderlessSection>
            ) : null}

            {isVideoPost(post) ? (
              <PostMedia media={post.media_attachments} post={post} isReblog={isReblog} />
            ) : (
              <GestureDetector gesture={tapComposition}>
                <View collapsable={false}>
                  {post.media_attachments.length > 1 ? (
                    <PostAlbumMedia
                      media={post.media_attachments}
                      post={post}
                      progress={progress}
                    />
                  ) : (
                    <PostMedia
                      media={post.media_attachments}
                      post={post}
                      isReblog={isReblog}
                    />
                  )}
                </View>
              </GestureDetector>
            )}
            {quoteNode}
            {!hideCaptions || isPermalink ? (
              <>
                <BorderlessSection>
                  <LinkPreviewCard card={post.card} />
                </BorderlessSection>
                <PostActions
                  variant="media"
                  hasLiked={post.favourited as boolean}
                  hasShared={post?.reblogged === true}
                  hasBookmarked={post?.bookmarked === true}
                  isLikeFeed={isLikeFeed}
                  post={post}
                  progress={progress}
                  isLikePending={isLikePending}
                  likesCount={post?.favourites_count}
                  likedBy={post?.liked_by}
                  sharesCount={post?.reblogs_count}
                  showAltText={showAltText}
                  commentsCount={getReplyCount(post)}
                  handleLike={handleLikeAction}
                  onOpenComments={onOpenCommentsForThis}
                  onShare={onShareForThis}
                />

                {isPermalink ? (
                  <PostCaption
                    postId={post.id}
                    caption={captionContent}
                    commentsCount={getReplyCount(post)}
                    createdAt={post.created_at}
                    tags={post.tags}
                    visibility={post.visibility}
                    disableReadMore={disableReadMore}
                    onOpenComments={onOpenCommentsForThis}
                    onHashtagPress={onHashtagPress}
                    onMentionPress={onMentionPress}
                    onUsernamePress={onUsernamePress}
                    editedAt={post.edited_at}
                    isLikeFeed={isLikeFeed}
                    likedAt={post.liked_at}
                    isPermalink={isPermalink}
                    emojis={post.emojis}
                    bodyAboveMedia={captionAboveMedia}
                  />
                ) : null}
              </>
            ) : null}
          </>
        ) : !hideCaptions || isPermalink ? (
          <TextPostBody
            post={post}
            user={user}
            isPermalink={isPermalink}
            isLikeFeed={isLikeFeed}
            booster={booster}
            menuContent={postMenuContent}
            disableReadMore={disableReadMore}
            captionHtml={captionContent ?? ''}
            quotedStatus={quoteNode}
            progress={progress}
            isLikePending={isLikePending}
            showAltText={showAltText}
            handleLike={handleLikeAction}
            onOpenComments={onOpenCommentsForThis}
            onShare={onShareForThis}
            onHashtagPress={onHashtagPress}
            onMentionPress={onMentionPress}
            onUsernamePress={onUsernamePress}
          />
        ) : null}
        {confirmDialog}
      </View>
    )
  },
  (prevProps, nextProps) => {
    // Custom comparison to prevent unnecessary re-renders. edited_at lets
    // server-side caption edits propagate without opening a render flood.
    // card (and reblog.card) compared by reference so link-preview crawls
    // that land after first render — local ~5s, federated 0–60s — repaint
    // the cell. React Query's default structuralSharing keeps unchanged
    // cards reference-stable, so a ref diff means the card actually moved.
    return (
      prevProps.post.id === nextProps.post.id &&
      prevProps.post.favourited === nextProps.post.favourited &&
      prevProps.post.favourites_count === nextProps.post.favourites_count &&
      prevProps.post.reblogged === nextProps.post.reblogged &&
      prevProps.post.bookmarked === nextProps.post.bookmarked &&
      prevProps.post.edited_at === nextProps.post.edited_at &&
      prevProps.post.card === nextProps.post.card &&
      prevProps.post.reblog?.card === nextProps.post.reblog?.card
    )
  }
)

export default FeedPost
