import { memo, useCallback, useMemo, useState } from 'react'
import type { Insets } from 'react-native'
import AutolinkText from 'src/components/common/AutolinkText'
import { PressableOpacity } from 'src/components/common/PressableOpacity'
import {
  menuSelectableStyle,
  useMenuSelectableTokens,
} from 'src/components/menu/selectable'
import { FootnoteText } from 'src/components/ui'
import type { CustomEmoji } from 'src/lib/api-types'
import {
  extractTrailingHashtags,
  htmlToTextWithLineBreaks,
  truncateKeepingShortcodes,
} from 'src/utils'
import { Text, View, XStack, YStack } from 'tamagui'

// Shared content renderer for every surface that previously paired
// `htmlToTextWithLineBreaks` with an `AutolinkText` inside a flat
// `<XStack flexWrap>`. That layout collapsed authored `\n\n` into a single
// line-height gap and dropped single-line breaks entirely on rewrap.
//
// Responsibilities:
//   1. HTML → text conversion (via `htmlToTextWithLineBreaks`).
//   2. Split on paragraph breaks (`\n\n+`) and render each paragraph as its
//      own `<AutolinkText>` inside a `<YStack>` with a vertical gap, so
//      paragraph separation is an actual layout gap instead of a single
//      collapsed line-height.
//   3. Optional character-count truncation with View More / View Less. The
//      threshold is measured against the joined body length; when possible
//      the cut lands on a paragraph boundary so the last visible paragraph
//      finishes cleanly, not mid-block.
//   4. Optional trailing-hashtag extraction — a "wall of hashtags" parked at
//      the end of a caption is pulled out and rendered as press-to-navigate
//      pills below the body.
//
// Consumers still call `preprocessStatusContent` themselves if they need to
// strip platform hint lines (quote-inline paragraph, Mobilizon event hints)
// before handing the html in.

const moreButtonHitSlop: Insets = { left: 15, right: 15, top: 6, bottom: 7 }

interface StatusContentProps {
  /** Raw HTML (or plain text) from the server. Converted internally via
   * `htmlToTextWithLineBreaks` so every caller stops hand-rolling that step. */
  html: string
  emojis?: CustomEmoji[]
  username?: string
  onUsernamePress?: () => void
  onHashtagPress: (hashtag: string) => void
  onMentionPress: (mention: string, isLocalMention: boolean) => void
  /** Character threshold for View More. Omit or pass `disableReadMore` to
   * render the full body every time. Captions default to 420, bios to 160,
   * comments to 280 — pass the number per call site. */
  thresholdChars?: number
  disableReadMore?: boolean
  /** When true, trailing hashtag-only lines at the end of the caption are
   * extracted and rendered as pills below the body. Inline hashtags stay
   * inline. Leave off for comments, bios, and long-form content. */
  trailingHashtagsAsPills?: boolean
  /** When true, the trailing hashtag pills are NOT rendered inside this
   * component — the caller is responsible for rendering them via
   * `useStatusHashtags(html, { enabled })`. Use this when the caption body
   * sits above media but the pills should sit below media (Mastodon
   * caption-above-media layout in FeedPost). The body is still extracted
   * from the trailing-hashtag tail so the visible text doesn't leak the
   * hashtag wall twice. */
  renderHashtagsExternally?: boolean
  /** Vertical gap between paragraphs. Defaults to `$2`. */
  paragraphGap?: string
  /**
   * When set, the View More / View Less expansion state is mirrored to a
   * module-level cache keyed by this string, so the state survives FlashList
   * recycler churn and `key={…}`-driven full remounts (e.g. profile-tab
   * switches that force the header to re-create). Pass `bio:<profile.id>`
   * for the profile bio; leave undefined for cells where the surrounding
   * post identity is enough to keep state local.
   */
  expansionKey?: string
}

// Module-level cache for the View More / View Less toggle. Each key maps to
// the most recent `expanded` value the user set. The cache is unbounded but
// effectively capped by the set of profiles/posts the user has interacted
// with in a session — small enough not to bother evicting.
const expansionCache = new Map<string, boolean>()

// Character-count truncation with a paragraph-aware cut. Returns the visible
// paragraph array plus a flag noting whether the content actually overflows.
function truncateToParagraphs(
  paragraphs: string[],
  thresholdChars: number
): { displayParagraphs: string[]; truncated: boolean } {
  const joinedLength =
    paragraphs.reduce((n, p) => n + p.length, 0) + Math.max(0, paragraphs.length - 1) * 2
  if (joinedLength <= thresholdChars) {
    return { displayParagraphs: paragraphs, truncated: false }
  }
  const kept: string[] = []
  let running = 0
  for (const p of paragraphs) {
    const nextLength = running + p.length + (kept.length > 0 ? 2 : 0)
    if (nextLength > thresholdChars) {
      // First paragraph overflows on its own — cut mid-paragraph; the
      // shared helper backs off any `:shortcode:` straddling the cut.
      if (kept.length === 0) {
        kept.push(truncateKeepingShortcodes(p, thresholdChars))
      }
      break
    }
    kept.push(p)
    running = nextLength
  }
  if (kept.length === 0) return { displayParagraphs: paragraphs, truncated: false }
  // Append an ellipsis to the last kept paragraph if it didn't already have
  // one (the mid-paragraph branch above already adds its own via
  // `truncateKeepingShortcodes`).
  const last = kept[kept.length - 1]
  if (!last.endsWith('…')) kept[kept.length - 1] = last.trimEnd() + '…'
  return { displayParagraphs: kept, truncated: true }
}

function StatusContentInner({
  html,
  emojis,
  username,
  onUsernamePress,
  onHashtagPress,
  onMentionPress,
  thresholdChars,
  disableReadMore,
  trailingHashtagsAsPills,
  renderHashtagsExternally,
  paragraphGap = '$2',
  expansionKey,
}: StatusContentProps) {
  const [expanded, setExpanded] = useState<boolean>(() =>
    expansionKey ? (expansionCache.get(expansionKey) ?? false) : false
  )

  const toggleExpanded = useCallback(() => {
    setExpanded((prev) => {
      const next = !prev
      if (expansionKey) expansionCache.set(expansionKey, next)
      return next
    })
  }, [expansionKey])

  const { bodyParagraphs, hashtags } = useMemo(() => {
    const text = htmlToTextWithLineBreaks(html ?? '') ?? ''
    const { body, hashtags } = trailingHashtagsAsPills
      ? extractTrailingHashtags(text)
      : { body: text, hashtags: [] as string[] }
    // Collapse runs of 3+ blank lines into a single paragraph break so mad
    // spacing authored in bios/captions doesn't translate to huge gaps.
    const normalized = body.replace(/\n{3,}/g, '\n\n')
    return {
      bodyParagraphs: normalized.split(/\n{2,}/).filter((p) => p.length > 0),
      hashtags,
    }
  }, [html, trailingHashtagsAsPills])

  const { displayParagraphs, truncated } = useMemo(() => {
    if (disableReadMore || !thresholdChars) {
      return { displayParagraphs: bodyParagraphs, truncated: false }
    }
    if (expanded) {
      return { displayParagraphs: bodyParagraphs, truncated: true }
    }
    return truncateToParagraphs(bodyParagraphs, thresholdChars)
  }, [bodyParagraphs, thresholdChars, disableReadMore, expanded])

  if (bodyParagraphs.length === 0 && hashtags.length === 0) return null

  return (
    <YStack gap={paragraphGap}>
      {displayParagraphs.length > 0 ? (
        <YStack gap={paragraphGap}>
          {displayParagraphs.map((paragraph, i) => (
            <AutolinkText
              key={`${i}-${paragraph.length}`}
              text={paragraph}
              emojis={emojis}
              username={i === 0 ? (username ?? '') : ''}
              onUsernamePress={i === 0 ? (onUsernamePress ?? noop) : noop}
              onHashtagPress={onHashtagPress}
              onMentionPress={onMentionPress}
            />
          ))}
        </YStack>
      ) : null}
      {truncated ? (
        <XStack>
          <PressableOpacity onPress={toggleExpanded} hitSlop={moreButtonHitSlop}>
            <FootnoteText tone="tertiary" emphasis="strong" py="$1">
              {expanded ? 'View Less' : 'View More'}
            </FootnoteText>
          </PressableOpacity>
        </XStack>
      ) : null}
      {hashtags.length > 0 && !renderHashtagsExternally ? (
        <HashtagPills hashtags={hashtags} onPress={onHashtagPress} />
      ) : null}
    </YStack>
  )
}

// Hook companion for `renderHashtagsExternally`. Runs the same trailing-
// hashtag extraction the component does internally so a caller hoisting the
// pills below media gets exactly the same list. Pure + cheap — the WeakMap
// cost lives in the shared `extractTrailingHashtags` helper.
export function useStatusHashtags(
  html: string | null | undefined,
  opts: { enabled: boolean }
): string[] {
  return useMemo(() => {
    if (!opts.enabled || !html) return []
    const text = htmlToTextWithLineBreaks(html) ?? ''
    return extractTrailingHashtags(text).hashtags
  }, [html, opts.enabled])
}

function noop() {}

// Memo: emojis and callbacks are typically stable per post (post identity is
// what changes). Skipping re-render when `html` and refs match mirrors the
// AutolinkText memo contract.
const StatusContent = memo(StatusContentInner, (prev, next) => {
  return (
    prev.html === next.html &&
    prev.emojis === next.emojis &&
    prev.thresholdChars === next.thresholdChars &&
    prev.disableReadMore === next.disableReadMore &&
    prev.trailingHashtagsAsPills === next.trailingHashtagsAsPills &&
    prev.renderHashtagsExternally === next.renderHashtagsExternally &&
    prev.username === next.username &&
    prev.onUsernamePress === next.onUsernamePress &&
    prev.onHashtagPress === next.onHashtagPress &&
    prev.onMentionPress === next.onMentionPress &&
    prev.paragraphGap === next.paragraphGap &&
    prev.expansionKey === next.expansionKey
  )
})

export default StatusContent

// Rendered below the caption body when the author parked a wall of hashtags
// at the end of their post. Borders + transparent fill match the `rest`
// tone of `menuSelectableStyle` so pills carry the same visual weight as
// unselected menu-surface controls.
export function HashtagPills({
  hashtags,
  onPress,
}: {
  hashtags: string[]
  onPress: (hashtag: string) => void
}) {
  const tokens = useMenuSelectableTokens()
  const restStyle = menuSelectableStyle('rest', tokens)
  if (hashtags.length === 0) return null
  return (
    <XStack flexWrap="wrap" gap="$2">
      {hashtags.map((tag) => (
        <PressableOpacity key={tag} onPress={() => onPress(tag)} hitSlop={4}>
          <View
            paddingHorizontal="$3"
            paddingVertical="$1.5"
            borderRadius={999}
            borderWidth={1}
            borderColor="$borderColor"
            backgroundColor={restStyle.backgroundColor}
          >
            <Text fontSize="$3" color={restStyle.iconTint}>
              #{tag}
            </Text>
          </View>
        </PressableOpacity>
      ))}
    </XStack>
  )
}
