import {
  type InfiniteData,
  useMutation,
  useQuery,
  useQueryClient,
} from '@tanstack/react-query'
import { type ErrorBoundaryProps, router, useLocalSearchParams } from 'expo-router'
import { useCallback, useEffect, useMemo } from 'react'
import ArticleDetailView from 'src/components/article/ArticleDetailView'
import { CommentThreadList } from 'src/components/comments/CommentThreadList'
import {
  FederatedRepliesPill,
  type PillState,
} from 'src/components/comments/FederatedRepliesPill'
import { ErrorState } from 'src/components/common/ErrorState'
import EventDetailView from 'src/components/event/EventDetailView'
import { ScreenScrollContainer } from 'src/components/layout'
import CommentItem from 'src/components/post/CommentItem'
import FeedPost from 'src/components/post/FeedPost'
import { useShareMutation } from 'src/hooks/mutations/useShareMutation'
import { PlaybackScope } from 'src/hooks/playback/PlaybackScope'
import { useRecursiveThreadContext } from 'src/hooks/useRecursiveThreadContext'
import { useThreadActions } from 'src/hooks/useThreadActions'
import { deleteStatusV1 } from 'src/lib/api'
import type { LoginUserResponse, Status } from 'src/lib/api-types'
import { useAsyncRefresh } from 'src/lib/asyncRefresh'
import { useFeature } from 'src/lib/features'
import { queryKeys } from 'src/lib/queries/keys'
import { patchAnchorReplyCount, postQueries } from 'src/lib/queries/posts'
import { pushReplyComposer } from 'src/lib/replyComposer'
import { emit } from 'src/lib/tapestry/emit'
import { useActiveProfile } from 'src/state/AuthProvider'
import { Spinner, Text, useTheme, View, YStack } from 'tamagui'

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

export default function Page() {
  return (
    <PlaybackScope name="post:detail">
      <PostContent />
    </PlaybackScope>
  )
}

function PostContent() {
  const { id } = useLocalSearchParams<{ id: string }>()
  const { profile: user } = useActiveProfile()
  const theme = useTheme()
  const queryClient = useQueryClient()
  const eventIngestEnabled = useFeature('event_ingest')
  const articleIngestEnabled = useFeature('article_ingest')
  const supportsRecursiveContext = useRecursiveThreadContext()

  useEffect(() => {
    emit('v')
  }, [])

  const { data, isLoading } = useQuery({
    ...postQueries.detail(id),
    placeholderData: () => {
      // Look through any cached list of posts (feeds, profile grids, hashtags,
      // search results) for this post so the detail renders instantly.
      const staticKeys = [
        postQueries.feed().queryKey,
        postQueries.networkFeed().queryKey,
        postQueries.selfBookmarks().queryKey,
        postQueries.selfLikes().queryKey,
      ]
      for (const key of staticKeys) {
        const feed = queryClient.getQueryData<InfiniteData<any>>(key)
        const found = findPostInPages(feed, id)
        if (found) return found
      }

      const dynamicPrefixes = [
        ['statusesById'],
        ['statusesFeedById'],
        ['getHashtagByNameFeed'],
        ['search'],
      ]
      for (const prefix of dynamicPrefixes) {
        const matches = queryClient.getQueriesData<InfiniteData<any>>({
          queryKey: prefix,
        })
        for (const [, feed] of matches) {
          const found = findPostInPages(feed, id)
          if (found) return found
        }
      }
      return undefined
    },
  })

  const isEvent =
    eventIngestEnabled && data?.pf_type === 'event' && (data as any)?.pf_event
  const isArticle =
    articleIngestEnabled && data?.pf_type === 'article' && (data as any)?.pf_article

  // The composer is no longer mounted on this screen — replies push the
  // modal `PostComposer` via `pushReplyComposer`. The hook now owns *only*
  // per-row mutations (like, delete) plus the confirm dialog render slot.
  const thread = useThreadActions({ statusId: id })

  // Events whose organiser disabled comments. Read directly from the
  // cached anchor — single source of truth, no duplicated state on the
  // hook side. Used to suppress the anchor message-icon below and surface
  // an inline "Comments disabled" pill above the (always-empty in this
  // case) descendants list.
  const eventCommentsDisabled =
    data?.pf_type === 'event' && (data as any)?.pf_event?.allow_comments === false

  // The thread always loads when there's an anchor — articles and events
  // render their detail body in place of the FeedPost anchor, with the
  // thread visible below. The article/event detail views' own "View
  // comments" affordances now no-op (or are hidden) since the thread is
  // already on-screen.
  const threadQuery = useQuery({
    ...postQueries.thread(id),
    enabled: !!data,
  })

  const ancestors = (threadQuery.data?.ancestors ?? []) as Status[]
  const descendants = (threadQuery.data?.descendants ?? []) as Status[]
  // `asyncRefresh` is set on remote anchors when the server has dispatched
  // a `FetchAllRepliesJob` (Halflight) — we poll `/context` until the
  // header drops off (the canonical completion signal, audit M6) and the
  // newly-fetched federated replies land in cache. Gated on the capability
  // hook so pre-deploy Pixelfed and sub-4.5 Mastodon stay on the
  // single-fetch path.
  const asyncRefresh = supportsRecursiveContext
    ? (threadQuery.data?.asyncRefresh ?? null)
    : null

  // Pill state machine. Today the post screen only constructs `idle` and
  // `fetching`; P8b will add `pending` and `complete` construction here
  // (no changes to the pill component itself — its discriminated union
  // already covers both).
  const pillState: PillState = useMemo(
    () =>
      asyncRefresh
        ? { kind: 'fetching', resultCount: asyncRefresh.resultCount }
        : { kind: 'idle' },
    [asyncRefresh]
  )

  // On Halflight-patched servers (and Mastodon ≥ 4.5) `/context` walks the
  // full ancestor chain — this fires only when `ANCESTORS_LIMIT=40` was hit.
  // On pre-deploy Pixelfed the server walks one hop, so this is the common
  // case. Tapping pushes the topmost ancestor as the new anchor.
  const hasMissingAncestor = !!ancestors[0]?.in_reply_to_id

  // Re-fetching `/context` is the only polling channel — Pixelfed has no
  // `/api/v1/async_refreshes/{id}` lookup endpoint, so the follow-up fetch
  // is itself the probe. When the server's job completes the header is
  // omitted, `asyncRefresh` flips to null, and `onComplete` fires once.
  useAsyncRefresh({
    spec: asyncRefresh,
    onPoll: () => {
      threadQuery.refetch()
    },
    onComplete: () => {
      // Belt-and-braces: the just-completed fetch already overwrote thread
      // cache, so this only matters for peer surfaces keyed by the same
      // anchor (e.g. a FeedPost row showing `replies_count`).
      queryClient.invalidateQueries({ queryKey: queryKeys.posts.thread(id) })
      // Patch the anchor's cached `posts.byId(id)` row directly with the
      // walked subtree size — invalidate alone re-fetches `byId`, which
      // returns the same stamped count. setQueryData is cheap and
      // flicker-free; thread queryFn also patches on its own onSuccess
      // path, so this catches the case where the federated walk grew
      // descendants between the last refetch and onComplete.
      const walked = threadQuery.data?.descendants?.length
      if (walked != null) patchAnchorReplyCount(id, walked)
    },
  })

  const onViewReplies = useCallback((row: Status) => {
    if (!row?.id) return
    router.push(`/post/${row.id}`)
  }, [])

  const onPushAncestor = useCallback(() => {
    const ancestor = ancestors[0]
    if (!ancestor) return
    router.push(`/post/${ancestor.in_reply_to_id ?? ancestor.id}`)
  }, [ancestors])

  const { handleShare } = useShareMutation()
  const onShare = (id: string, state: any) => {
    handleShare(id, state === true)
  }

  const onDeletePost = (id: string) => {
    deletePostMutation.mutate(id)
  }

  const deletePostMutation = useMutation({
    mutationFn: async (id: string) => {
      return await deleteStatusV1(id)
    },
    onSuccess: () => {
      emit('d')
      router.replace('/(auth)/(tabs)/home')
    },
  })

  // The anchor's PostActions comment icon used to focus the sticky
  // composer here; now it pushes the modal compose pre-targeted at the
  // anchor. On every other surface (feed/profile/search) the same icon
  // still pushes `/post/[id]` to open the conversation — that wiring is
  // upstream and unchanged.
  const onReplyToAnchor = useCallback(() => {
    if (!data) return
    pushReplyComposer(data as Status)
  }, [data])

  const gotoProfile = useCallback(
    (userId: string) => router.push(`/profile/${userId}`),
    []
  )
  const gotoHashtag = useCallback(
    (hashtag: string) => router.push(`/hashtag/${hashtag}`),
    []
  )
  const gotoUsernameProfile = useCallback(
    (name: string) => router.push(`/profile/0?byUsername=${name.slice(1)}`),
    []
  )
  const gotoLikes = useCallback(
    (postId: string) => router.push(`/post/likes/${postId}`),
    []
  )
  const gotoReport = useCallback(
    (postId: string) => router.push(`/post/report/${postId}`),
    []
  )

  if (isLoading && !data) {
    return (
      <ScreenScrollContainer variant="scroll">
        <View flexGrow={1} alignItems="center" justifyContent="center">
          <Spinner color="$color" />
        </View>
      </ScreenScrollContainer>
    )
  }

  if (!data) return null

  // Anchor body — the FeedPost render for plain statuses, the dedicated
  // article/event detail view when the underlying status is one of those
  // types. Same screen on every type so the thread always sits below the
  // anchor, matching how mastodon-ios anchors any /context view.
  const AnchorBody = isEvent ? (
    <EventDetailView post={data} hideCommentsLink />
  ) : isArticle ? (
    <ArticleDetailView post={data} hideCommentsLink />
  ) : (
    <FeedPost
      post={data}
      user={user as LoginUserResponse}
      onOpenComments={onReplyToAnchor}
      onDeletePost={onDeletePost}
      disableReadMore={true}
      isPermalink={true}
      onShare={() => onShare(data?.id, data?.reblogged)}
    />
  )

  const ListHeader = (
    <YStack>
      {hasMissingAncestor ? (
        <View
          px="$3"
          py="$2"
          borderBottomWidth={1}
          borderColor={theme.borderColor.val}
          backgroundColor={theme.background.val}
        >
          <Text
            color={theme.interactiveActive.val}
            fontWeight="600"
            onPress={onPushAncestor}
          >
            Show full conversation
          </Text>
        </View>
      ) : null}
      {ancestors.map((ancestor) => (
        <AncestorRow
          key={ancestor.id}
          status={ancestor}
          onPress={() => onViewReplies(ancestor)}
          gotoProfile={gotoProfile}
          gotoUsernameProfile={gotoUsernameProfile}
          gotoHashtag={gotoHashtag}
          onLike={thread.likeRow}
          onReply={pushReplyComposer}
          onShowLikes={gotoLikes}
          onReport={gotoReport}
          onDelete={thread.deleteRow}
          user={user}
        />
      ))}
      <View
        borderTopWidth={ancestors.length > 0 ? 4 : 0}
        borderBottomWidth={descendants.length > 0 ? 4 : 0}
        borderColor={theme.borderColor.val}
      >
        {AnchorBody}
      </View>
      <FederatedRepliesPill state={pillState} />
      {eventCommentsDisabled ? (
        <View
          px="$3"
          py="$2"
          borderBottomWidth={1}
          borderColor={theme.borderColor.val}
          backgroundColor={theme.background.val}
        >
          <Text fontSize="$2" color={theme.colorTertiary.val} textAlign="center">
            Comments disabled by the organiser.
          </Text>
        </View>
      ) : null}
    </YStack>
  )

  return (
    <ScreenScrollContainer variant="flashlist">
      {(scrollProps) => (
        <View style={{ flex: 1 }}>
          <CommentThreadList
            comments={descendants}
            anchorId={id}
            user={user}
            ListHeaderComponent={ListHeader}
            scrollProps={scrollProps}
            onReply={pushReplyComposer}
            onLike={thread.likeRow}
            onReport={gotoReport}
            onDelete={thread.deleteRow}
            onShowLikes={gotoLikes}
            onViewReplies={onViewReplies}
            gotoProfile={gotoProfile}
            gotoUsernameProfile={gotoUsernameProfile}
            gotoHashtag={gotoHashtag}
          />
          {thread.confirmDialog}
        </View>
      )}
    </ScreenScrollContainer>
  )
}

// Ancestor rows reuse CommentItem to keep the visual rhythm consistent.
// They always expose a "Open this reply →" affordance — the row itself
// isn't a Pressable, matching descendants — so the only way to navigate
// up the chain is the explicit link.
function AncestorRow({
  status,
  onPress,
  ...rest
}: {
  status: Status
  onPress: () => void
} & Pick<
  React.ComponentProps<typeof CommentThreadList>,
  | 'gotoProfile'
  | 'gotoUsernameProfile'
  | 'gotoHashtag'
  | 'onLike'
  | 'onReply'
  | 'onShowLikes'
  | 'onReport'
  | 'onDelete'
  | 'user'
>) {
  return (
    <CommentItem
      item={status}
      user={rest.user}
      onReply={rest.onReply}
      onLike={rest.onLike}
      onReport={rest.onReport}
      onDelete={rest.onDelete}
      onShowLikes={rest.onShowLikes}
      onViewReplies={onPress}
      viewRepliesLabel="Open this reply →"
      gotoProfile={rest.gotoProfile}
      gotoUsernameProfile={rest.gotoUsernameProfile}
      gotoHashtag={rest.gotoHashtag}
    />
  )
}

function findPostInPages(feed: InfiniteData<any> | undefined, id: string) {
  for (const page of feed?.pages ?? []) {
    // Three shapes we walk through: a flat array (most feed pages), a
    // `{ data, nextPage }` Link-paginated envelope, or the protocol-adapter
    // `{ items, nextCursor }` shape (hashtag feed, post-Phase 2).
    const items = Array.isArray(page) ? page : (page.items ?? page.data ?? [])
    const found = items.find((p: any) => p.id === id)
    if (found) return found
  }
  return undefined
}
