import { useRef, useState, useEffect, useMemo } from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  Dimensions,
} from 'react-native';
import { Image } from 'expo-image';
import * as Clipboard from 'expo-clipboard';
import { Ionicons } from '@expo/vector-icons';
import { Audio } from 'expo-av';
import { useTranslation } from 'react-i18next';
import { useColors } from '../../lib/theme';
import { useThemeStore } from '../../stores/theme';
import { UserAvatar } from '../UserAvatar';
import { MessageActionMenu, type AnchorRect } from './MessageActionMenu';

function fmtSec(s: number): string {
  const m = Math.floor(s / 60);
  const sec = (s % 60).toString().padStart(2, '0');
  return `${m}:${sec}`;
}

const SCREEN_W = Dimensions.get('window').width;

function VoiceNoteBubble({ url, duration, isOwn }: { url: string; duration: string; isOwn: boolean }) {
  const colors = useColors();
  const bubbleColors = useBubbleColors();
  const [isPlaying, setIsPlaying] = useState(false);
  const [progress, setProgress] = useState(0);
  const [currentTime, setCurrentTime] = useState(0);
  const soundRef = useRef<Audio.Sound | null>(null);
  const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
  // Merkt sich ob die Wiedergabe komplett durchlief — dann muss der nächste
  // Play von vorne (replayAsync) statt am Ende-stehengebliebenen playAsync.
  const finishedRef = useRef(false);

  const totalSeconds = useMemo(() => {
    const [m, s] = (duration ?? '0:00').split(':').map(Number);
    return (m || 0) * 60 + (s || 0);
  }, [duration]);

  // WhatsApp-Look: ~34 dickere Balken mit deutlich variierender Höhe statt
  // 80 gleichförmiger dünner Striche (sah „hardcodiert" aus). Deterministischer
  // LCG-PRNG (aus URL geseedet) → pro Sprachnachricht stabil, aber natürliche
  // Amplituden-Streuung wie eine echte Sprach-Wellenform.
  const barHeights = useMemo(() => {
    let s = url.split('').reduce((acc, c) => (acc * 31 + c.charCodeAt(0)) >>> 0, 7) || 1;
    const rand = () => {
      s = (s * 1103515245 + 12345) >>> 0;
      return s / 0xffffffff;
    };
    const MAX_H = 24;
    return Array.from({ length: 34 }, (_, i) => {
      // Sanfte Sprech-Hüllkurve (steigt/fällt) × Zufalls-Spitzen
      const env = 0.45 + 0.55 * Math.abs(Math.sin((i / 34) * Math.PI * 2.3 + (s % 5)));
      const peak = 0.2 + 0.8 * rand();
      return Math.max(3, peak * env * MAX_H);
    });
  }, [url]);

  useEffect(() => {
    return () => {
      if (pollRef.current) clearInterval(pollRef.current);
      soundRef.current?.unloadAsync();
    };
  }, []);

  async function togglePlay() {
    if (isPlaying) {
      await soundRef.current?.pauseAsync();
      if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
      setIsPlaying(false);
      return;
    }
    try {
      if (!soundRef.current) {
        await Audio.setAudioModeAsync({ allowsRecordingIOS: false, playsInSilentModeIOS: true });
        const { sound } = await Audio.Sound.createAsync({ uri: url }, { shouldPlay: true });
        soundRef.current = sound;
        finishedRef.current = false;
        sound.setOnPlaybackStatusUpdate((s) => {
          if (s.isLoaded && s.didJustFinish) {
            finishedRef.current = true;
            setIsPlaying(false);
            setProgress(0);
            setCurrentTime(0);
            if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
          }
        });
      } else if (finishedRef.current) {
        // Nach komplettem Durchlauf: von vorne abspielen (Position steht am Ende)
        finishedRef.current = false;
        setProgress(0);
        setCurrentTime(0);
        await soundRef.current.replayAsync();
      } else {
        // Resume nach Pause: Position beibehalten
        await soundRef.current.playAsync();
      }
      setIsPlaying(true);
      pollRef.current = setInterval(async () => {
        const s = await soundRef.current?.getStatusAsync();
        if (s?.isLoaded) {
          setCurrentTime(Math.floor(s.positionMillis / 1000));
          setProgress(s.durationMillis ? s.positionMillis / s.durationMillis : 0);
        }
      }, 100);
    } catch (err) {
      console.warn('[VoiceNote] play error:', err);
    }
  }

  const playedCount = Math.floor(progress * barHeights.length);

  // Insta-Style Wellenform-Farben:
  //  - eigene Bubble (Mint-BG): Inhalt weiß, gespielte Bars weiß, ungespielte
  //    halbtransparent-weiß (grau wirkend).
  //  - fremde Bubble (graue Clean-Bubble): Inhalt schwarz, gespielte Bars
  //    schwarz, ungespielte grau.
  // Im Ruhezustand (noch nicht gestartet ODER nach komplettem Durchlauf) sind
  // ALLE Bars in der Vollfarbe (schwarz / weiß) — die Zwei-Ton-Progress-Ansicht
  // erscheint nur während/nach dem Abspielen.
  const fullBarColor = isOwn ? '#ffffff' : '#0a0a0a';
  const dimBarColor = isOwn ? 'rgba(255,255,255,0.42)' : 'rgba(0,0,0,0.26)';
  const showFullBars = !isPlaying && progress === 0;
  const playBtnBg = isOwn ? 'rgba(255,255,255,0.22)' : 'rgba(0,0,0,0.06)';
  const playIconColor = isOwn ? '#ffffff' : colors.text;
  const durationColor = isOwn ? 'rgba(255,255,255,0.9)' : colors.textMuted;
  const displayDuration = isPlaying ? fmtSec(currentTime) : (duration || fmtSec(totalSeconds));

  const bubbleW = Math.floor(SCREEN_W * 0.60);

  return (
    <View style={{ width: bubbleW, paddingVertical: 2, paddingHorizontal: 0 }}>
      <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
        <TouchableOpacity onPress={togglePlay} activeOpacity={0.7} hitSlop={8}>
          <View style={{ width: 30, height: 30, borderRadius: 15, backgroundColor: playBtnBg, alignItems: 'center', justifyContent: 'center' }}>
            <Ionicons name={isPlaying ? 'pause' : 'play'} size={13} color={playIconColor} style={{ marginLeft: isPlaying ? 0 : 1 }} />
          </View>
        </TouchableOpacity>

        <View style={{ flex: 1, height: 26, position: 'relative' }}>
          <View style={{ flexDirection: 'row', alignItems: 'center', height: '100%', justifyContent: 'space-between' }}>
            {barHeights.map((h, i) => (
              <View
                key={i}
                style={{ width: 3, height: h, borderRadius: 1.5, backgroundColor: showFullBars ? fullBarColor : (i < playedCount ? fullBarColor : dimBarColor) }}
              />
            ))}
          </View>
        </View>
      </View>

      <Text style={{ fontSize: 10, fontFamily: 'Nunito_600SemiBold', color: durationColor, marginTop: 2, marginLeft: 36, fontVariant: ['tabular-nums'] }}>
        {displayDuration}
      </Text>
    </View>
  );
}

export type MessageReaction = { emoji: string; count: number; mine: boolean };
export type ChatMsg = {
  id: string;
  userId: string;
  nickname?: string | null;
  avatar?: string | null;
  content: string;
  replyTo?: {
    id: string;
    userId: string;
    nickname?: string | null;
    content: string;
    attachmentType?: string | null;
  } | null;
  attachmentUrl?: string | null;
  attachmentType?: string | null;
  attachmentName?: string | null;
  likesCount: number;
  likedByMe?: boolean;
  createdAt: string;
  isOwn: boolean;
  readAt?: string | null;
  /** Aggregierte Emoji-Reaktionen (DM). */
  reactions?: MessageReaction[];
  /** Soft-Delete-Tombstone. */
  deleted?: boolean;
  /** Optimistic-UI Status (pending = wird gesendet, failed = Fehler). */
  status?: 'pending' | 'sent' | 'failed';
};

type Props = {
  msg: ChatMsg;
  showName?: boolean;
  isFirstInGroup?: boolean;
  isLastInGroup?: boolean;
  hideReadStatus?: boolean;
  /** Direct-Message-Mode: Likes als boolean-Herz (Insta-Style) statt Count, kein Avatar-Spalte-Whatever */
  isDM?: boolean;
  onReply: (msg: ChatMsg) => void;
  onLike: (msg: ChatMsg) => void;
  /** DM-only: Emoji-Reaktion togglen. Group-Chat (deaktiviert) übergibt nichts. */
  onReact?: (msg: ChatMsg, emoji: string) => void;
  /** DM-only: eigene Nachricht löschen (Soft-Delete). */
  onDelete?: (msg: ChatMsg) => void;
  onOpenImage: (url: string) => void;
  /** Clean-Background-Mode (weißer Chat-BG): eingehende Bubbles bekommen einen
   *  leichten Grauton statt Weiß, damit sie sich vom Hintergrund abheben (Insta-Style). */
  cleanBg?: boolean;
};

function formatTime(ts: string) {
  return new Date(ts).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
}

function useBubbleColors() {
  const colorScheme = useThemeStore((s) => s.colorScheme);
  const isDark = colorScheme === 'dark';
  return {
    ownBg: isDark ? '#1e4d3a' : '#D1F4CC',
    // Eigene Voice-Bubble: kräftiges Mint-Grün (Insta-Style), weißer Inhalt.
    ownAudioBg: isDark ? '#1f7a63' : '#34C7A0',
    ownText: isDark ? '#e8f5e2' : '#0a0a0a',
    otherBg: isDark ? '#2c2c2e' : '#ffffff',
    otherAudioBg: isDark ? '#2c2c2e' : '#ffffff',
    otherText: isDark ? '#ffffff' : '#0a0a0a',
    replyBarColor: '#25D366',
    readColor: '#34B7F1',
  };
}

function CallNoteRow({ msg }: { msg: ChatMsg }) {
  const { t } = useTranslation();
  const colors = useColors();
  const [, stateRaw, durRaw] = (msg.attachmentName ?? 'audio:ended:0').split(':');
  const state = (stateRaw || 'ended') as 'ended' | 'unanswered' | 'declined' | 'failed' | 'busy';
  const durSec = parseInt(durRaw || '0', 10) || 0;
  const isMissed = state !== 'ended';
  const iconColor = isMissed ? '#ef4444' : (msg.isOwn ? '#10b981' : colors.text);
  let label: string;
  if (!isMissed) {
    const m = Math.floor(durSec / 60);
    const s = (durSec % 60).toString().padStart(2, '0');
    label = `${t('chat.call_audio')} · ${m}:${s}`;
  } else if (state === 'declined') {
    label = msg.isOwn ? t('chat.call_declined') : t('chat.call_missed');
  } else if (state === 'unanswered') {
    label = msg.isOwn ? t('chat.call_no_answer') : t('chat.call_missed');
  } else {
    label = t('chat.call_failed');
  }
  const time = new Date(msg.createdAt).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
  return (
    <View style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center', paddingVertical: 6, paddingHorizontal: 16 }}>
      <View style={{ flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: colors.surfaceElevated, paddingHorizontal: 12, paddingVertical: 8, borderRadius: 16 }}>
        <View style={{ width: 28, height: 28, borderRadius: 14, backgroundColor: 'rgba(0,0,0,0.06)', alignItems: 'center', justifyContent: 'center' }}>
          <Ionicons name="call-outline" size={15} color={iconColor} />
        </View>
        <View>
          <Text style={{ fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: colors.text }}>{label}</Text>
          <Text style={{ fontSize: 11, color: colors.textMuted, marginTop: 1 }}>{time}</Text>
        </View>
      </View>
    </View>
  );
}

export function ChatBubble(props: Props) {
  // Call-Notiz (System-Row, kein Bubble) — eigenes Render-Path, ohne Hooks-Aufwand.
  if (props.msg.attachmentType === 'call') {
    return <CallNoteRow msg={props.msg} />;
  }
  return <ChatBubbleInner {...props} />;
}

function ChatBubbleInner({
  msg,
  showName = false,
  isFirstInGroup = true,
  isLastInGroup = true,
  hideReadStatus = false,
  isDM = false,
  onReply,
  onLike,
  onReact,
  onDelete,
  onOpenImage,
  cleanBg = false,
}: Props) {
  const { t } = useTranslation();
  const colors = useColors();
  const styles = makeStyles(colors);
  const bubbleColors = useBubbleColors();
  const bubbleRef = useRef<View>(null);
  const [menuVisible, setMenuVisible] = useState(false);
  const [anchor, setAnchor] = useState<AnchorRect | null>(null);
  const hasContent = msg.content !== '';
  const myReaction = msg.reactions?.find((r) => r.mine)?.emoji ?? null;

  function openActions() {
    if (msg.deleted) return; // gelöschte Nachrichten: kein Kontextmenü
    bubbleRef.current?.measureInWindow((x, y, width, height) => {
      setAnchor({ x, y, width, height });
      setMenuVisible(true);
    });
  }

  const isImageOnly =
    !!msg.attachmentUrl && msg.attachmentType === 'image' && !msg.content && !msg.replyTo;
  // isAudioMsg: hat Audio-Attachment (unabhängig von replyTo/content — für Styling)
  const isAudioMsg = !!msg.attachmentUrl && msg.attachmentType === 'audio';
  const isAudioOnly = isAudioMsg && !msg.content && !msg.replyTo;
  const replyHasImage = msg.replyTo?.attachmentType === 'image';
  const replyHasAudio = msg.replyTo?.attachmentType === 'audio';
  const ownBubbleRadius = {
    borderTopLeftRadius: 14,
    borderTopRightRadius: isFirstInGroup ? 14 : 4,
    borderBottomLeftRadius: 14,
    borderBottomRightRadius: isLastInGroup ? 4 : 4,
  };

  const otherBubbleRadius = {
    borderTopLeftRadius: isFirstInGroup ? 14 : 4,
    borderTopRightRadius: 14,
    borderBottomLeftRadius: isLastInGroup ? 4 : 4,
    borderBottomRightRadius: 14,
  };

  // Clean-Mode + Light: eingehende Bubbles leicht grau (#f0f0f2) statt reinweiß,
  // sonst verschwinden sie auf dem weißen Clean-Background. Dark-Mode braucht das
  // nicht (otherBg #2c2c2e hebt sich schon vom schwarzen BG ab).
  const colorScheme = useThemeStore((s) => s.colorScheme);
  const otherBgEff =
    cleanBg && colorScheme !== 'dark' ? '#EFEFF1' : bubbleColors.otherBg;
  const otherAudioBgEff =
    cleanBg && colorScheme !== 'dark' ? '#EFEFF1' : bubbleColors.otherAudioBg;
  const bubbleBg = msg.isOwn
    ? (isAudioMsg ? bubbleColors.ownAudioBg : bubbleColors.ownBg)
    : (isAudioMsg ? otherAudioBgEff : otherBgEff);
  const bubbleText = msg.isOwn ? bubbleColors.ownText : bubbleColors.otherText;

  function copyContent() {
    if (msg.content) Clipboard.setStringAsync(msg.content);
  }

  // Scharfe Kopie der Bubble fürs Kontextmenü (bleibt über dem Blur sichtbar).
  const previewNode = (
    <View
      style={[
        styles.bubble,
        msg.isOwn ? ownBubbleRadius : otherBubbleRadius,
        { backgroundColor: bubbleBg },
        !msg.isOwn && styles.bubbleOtherBorder,
        isImageOnly && { padding: 4 },
        isAudioMsg && { paddingHorizontal: 6, paddingTop: 6, paddingBottom: 4 },
      ]}
    >
      {msg.attachmentUrl && msg.attachmentType === 'image' ? (
        <Image source={{ uri: msg.attachmentUrl }} style={styles.image} contentFit="cover" cachePolicy="memory-disk" />
      ) : msg.attachmentUrl && msg.attachmentType === 'audio' ? (
        <VoiceNoteBubble url={msg.attachmentUrl} duration={msg.attachmentName ?? '0:00'} isOwn={msg.isOwn} />
      ) : msg.content !== '' ? (
        <Text style={[styles.content, { color: bubbleText }]}>{msg.content}</Text>
      ) : null}
    </View>
  );

  return (
    <>
      <View
        style={[
          styles.row,
          { justifyContent: msg.isOwn ? 'flex-end' : 'flex-start' },
          { marginTop: isFirstInGroup ? 8 : 2 },
        ]}
      >
        {!msg.isOwn && (
          <View style={styles.avatarSlot}>
            {isLastInGroup ? (
              <UserAvatar
                userId={msg.userId}
                avatar={msg.avatar ?? null}
                nickname={msg.nickname ?? '?'}
                size="sm"
                showOnlineIndicator={false}
              />
            ) : null}
          </View>
        )}

        <View style={[styles.bubbleCol, { alignItems: msg.isOwn ? 'flex-end' : 'flex-start' }, isAudioMsg && styles.bubbleColAudio]}>
          {showName && !msg.isOwn && isFirstInGroup && (
            <Text style={styles.nickname} numberOfLines={1}>
              {msg.nickname ?? '?'}
            </Text>
          )}

          <TouchableOpacity
            ref={bubbleRef}
            delayLongPress={350}
            onLongPress={openActions}
            activeOpacity={1}
            style={[
              styles.bubble,
              msg.isOwn ? ownBubbleRadius : otherBubbleRadius,
              { backgroundColor: bubbleBg },
              !msg.isOwn && styles.bubbleOtherBorder,
              isImageOnly && { padding: 4 },
              isAudioMsg && { paddingHorizontal: 6, paddingTop: 6, paddingBottom: 4 },
              msg.status === 'pending' && { opacity: 0.6 },
              msg.status === 'failed' && { borderWidth: 1, borderColor: '#ef4444' },
            ]}
          >
            {msg.replyTo && (
              <TouchableOpacity
                activeOpacity={0.7}
                style={[
                  styles.replyPreview,
                  {
                    backgroundColor: msg.isOwn
                      ? 'rgba(0,0,0,0.08)'
                      : colors.surfaceElevated,
                    borderLeftColor: bubbleColors.replyBarColor,
                  },
                ]}
              >
                <Text
                  style={{
                    fontSize: 11,
                    fontFamily: 'Nunito_700Bold',
                    color: bubbleColors.replyBarColor,
                  }}
                  numberOfLines={1}
                >
                  {msg.replyTo.nickname ?? '?'}
                </Text>
                <Text
                  style={{
                    fontSize: 11,
                    fontFamily: 'Nunito_400Regular',
                    color: colors.textMuted,
                    marginTop: 1,
                  }}
                  numberOfLines={1}
                >
                  {replyHasImage && (
                    <Ionicons name="image" size={11} color={colors.textMuted} />
                  )}
                  {replyHasAudio && (
                    <Ionicons name="mic" size={11} color={colors.textMuted} />
                  )}{' '}
                  {msg.replyTo.content || (replyHasImage ? t('chat.image_attachment') : replyHasAudio ? `🎤 ${t('chat.voice_message')}` : '…')}
                </Text>
              </TouchableOpacity>
            )}

            {msg.attachmentUrl && msg.attachmentType === 'image' && (
              <TouchableOpacity
                onPress={() => onOpenImage(msg.attachmentUrl!)}
                activeOpacity={0.7}
                style={[styles.imageWrap, msg.content ? { marginBottom: 4 } : null]}
              >
                <Image
                  source={{ uri: msg.attachmentUrl }}
                  style={styles.image}
                  contentFit="cover"
                  cachePolicy="memory-disk"
                  transition={200}
                />
                {isImageOnly && (
                  <View style={styles.imageTimeOverlay}>
                    {!isDM && msg.likesCount > 0 && (
                      <View style={{ flexDirection: 'row', alignItems: 'center', marginRight: 4 }}>
                        <Ionicons name="heart" size={10} color="#f87171" />
                        <Text style={{ fontSize: 10, color: '#fff', marginLeft: 2 }}>
                          {msg.likesCount}
                        </Text>
                      </View>
                    )}
                    <Text style={{ fontSize: 10, color: '#fff' }}>{formatTime(msg.createdAt)}</Text>
                  </View>
                )}
              </TouchableOpacity>
            )}

            {msg.attachmentUrl && msg.attachmentType === 'audio' && (
              <VoiceNoteBubble
                url={msg.attachmentUrl}
                duration={msg.attachmentName ?? '0:00'}
                isOwn={msg.isOwn}
              />
            )}

            {msg.attachmentUrl && msg.attachmentType !== 'image' && msg.attachmentType !== 'audio' && (
              <View
                style={{
                  flexDirection: 'row',
                  alignItems: 'center',
                  paddingHorizontal: 10,
                  paddingVertical: 8,
                  borderRadius: 10,
                  marginBottom: 4,
                  backgroundColor: 'rgba(0,0,0,0.06)',
                }}
              >
                <Ionicons name="document-attach" size={18} color={colors.textMuted} />
                <Text
                  style={{
                    fontSize: 12,
                    fontFamily: 'Nunito_600SemiBold',
                    marginLeft: 8,
                    color: bubbleText,
                    flex: 1,
                  }}
                  numberOfLines={1}
                >
                  {msg.attachmentName ?? t('chat.file_attachment')}
                </Text>
              </View>
            )}

            {msg.deleted ? (
              <Text style={[styles.content, { color: bubbleText, fontStyle: 'italic', opacity: 0.6 }]}>
                {t('chat.message_deleted')}
              </Text>
            ) : msg.content !== '' ? (
              <Text style={[styles.content, { color: bubbleText }]}>
                {msg.content}
              </Text>
            ) : null}

            {!isImageOnly && (
              <View style={styles.footer}>
                {!isDM && msg.likesCount > 0 && (
                  <View style={{ flexDirection: 'row', alignItems: 'center', marginRight: 4 }}>
                    <Ionicons name="heart" size={10} color="#f87171" />
                    <Text
                      style={{
                        fontSize: 10,
                        marginLeft: 2,
                        fontFamily: 'Nunito_600SemiBold',
                        color: colors.textMuted,
                      }}
                    >
                      {msg.likesCount}
                    </Text>
                  </View>
                )}
                <Text
                  style={{
                    fontSize: 10,
                    fontFamily: 'Nunito_400Regular',
                    color: msg.isOwn ? 'rgba(0,0,0,0.45)' : colors.textMuted,
                  }}
                >
                  {formatTime(msg.createdAt)}
                </Text>
                {isDM && msg.isOwn && msg.status !== 'pending' && msg.status !== 'failed' && (
                  <Ionicons
                    name={msg.readAt ? 'checkmark-done' : 'checkmark'}
                    size={12}
                    color={msg.readAt ? bubbleColors.readColor : 'rgba(0,0,0,0.35)'}
                    style={{ marginLeft: 2 }}
                  />
                )}
                {msg.status === 'pending' && (
                  <Ionicons
                    name="time-outline"
                    size={11}
                    color="rgba(0,0,0,0.35)"
                    style={{ marginLeft: 2 }}
                  />
                )}
                {msg.status === 'failed' && (
                  <Ionicons
                    name="alert-circle"
                    size={11}
                    color="#ef4444"
                    style={{ marginLeft: 2 }}
                  />
                )}
              </View>
            )}
          </TouchableOpacity>

          {/* Insta-Style: kleines Herz-Badge hängt unter der Bubble (nur DM, nur wenn liked) */}
          {isDM && msg.likedByMe && (
            <TouchableOpacity
              onPress={() => onLike(msg)}
              activeOpacity={0.7}
              hitSlop={8}
              style={[
                styles.dmHeartBadge,
                {
                  alignSelf: msg.isOwn ? 'flex-end' : 'flex-start',
                  marginRight: msg.isOwn ? 8 : 0,
                  marginLeft: msg.isOwn ? 0 : 8,
                },
              ]}
            >
              <Ionicons name="heart" size={12} color="#f87171" />
            </TouchableOpacity>
          )}

          {/* Emoji-Reaktions-Pills unter der Bubble (DM) */}
          {msg.reactions && msg.reactions.length > 0 && (
            <View
              style={[
                styles.reactionPills,
                { alignSelf: msg.isOwn ? 'flex-end' : 'flex-start' },
              ]}
            >
              {msg.reactions.map((r) => (
                <TouchableOpacity
                  key={r.emoji}
                  onPress={() => onReact?.(msg, r.emoji)}
                  activeOpacity={0.6}
                  style={styles.reactionPill}
                >
                  <Text style={{ fontSize: 16 }}>{r.emoji}</Text>
                  {r.count > 1 && <Text style={styles.reactionPillCount}>{r.count}</Text>}
                </TouchableOpacity>
              ))}
            </View>
          )}
        </View>
      </View>

      <MessageActionMenu
        visible={menuVisible}
        anchor={anchor}
        isOwn={msg.isOwn}
        hasContent={hasContent}
        myReaction={myReaction}
        preview={previewNode}
        onClose={() => setMenuVisible(false)}
        onReact={(emoji) => onReact?.(msg, emoji)}
        onReply={() => onReply(msg)}
        onCopy={copyContent}
        onDelete={() => onDelete?.(msg)}
      />
    </>
  );
}

function makeStyles(colors: ReturnType<typeof useColors>) {
  return StyleSheet.create({
    row: {
      flexDirection: 'row',
      paddingHorizontal: 10,
    },
    avatarSlot: {
      width: 32,
      marginRight: 6,
      justifyContent: 'flex-end',
    },
    reactionPills: {
      flexDirection: 'row',
      flexWrap: 'wrap',
      gap: 4,
      marginTop: -6,
      marginBottom: 2,
    },
    reactionPill: {
      flexDirection: 'row',
      alignItems: 'center',
      paddingHorizontal: 2,
    },
    reactionPillCount: {
      fontSize: 11,
      marginLeft: 3,
      fontFamily: 'Nunito_600SemiBold',
      color: colors.textMuted,
    },
    bubbleCol: {
      maxWidth: '76%',
    },
    bubbleColAudio: {
      maxWidth: '84%',
    },
    nickname: {
      fontSize: 11,
      fontFamily: 'Nunito_700Bold',
      color: '#25D366',
      marginBottom: 3,
      marginLeft: 12,
    },
    bubble: {
      paddingHorizontal: 12,
      paddingTop: 8,
      paddingBottom: 6,
      shadowColor: '#000',
      shadowOpacity: 0.08,
      shadowRadius: 3,
      shadowOffset: { width: 0, height: 1 },
      elevation: 1,
    },
    bubbleOtherBorder: {
      borderWidth: StyleSheet.hairlineWidth,
      borderColor: 'rgba(0,0,0,0.06)',
    },
    replyPreview: {
      borderLeftWidth: 4,
      borderRadius: 6,
      paddingHorizontal: 8,
      paddingVertical: 4,
      marginBottom: 6,
    },
    imageWrap: {
      borderRadius: 10,
      overflow: 'hidden',
      position: 'relative',
    },
    image: {
      width: 220,
      height: 220,
      backgroundColor: colors.surfaceElevated,
    },
    imageTimeOverlay: {
      position: 'absolute',
      bottom: 6,
      right: 6,
      backgroundColor: 'rgba(0,0,0,0.5)',
      borderRadius: 10,
      paddingHorizontal: 6,
      paddingVertical: 2,
      flexDirection: 'row',
      alignItems: 'center',
    },
    content: {
      fontSize: 15,
      lineHeight: 22,
      fontFamily: 'Nunito_400Regular',
    },
    footer: {
      flexDirection: 'row',
      alignItems: 'center',
      justifyContent: 'flex-end',
      gap: 3,
      marginTop: 4,
      alignSelf: 'flex-end',
    },
    dmHeartBadge: {
      marginTop: -6,
      backgroundColor: colors.bg,
      borderRadius: 999,
      paddingHorizontal: 4,
      paddingVertical: 3,
      shadowColor: '#000',
      shadowOpacity: 0.12,
      shadowOffset: { width: 0, height: 1 },
      shadowRadius: 2,
      elevation: 2,
    },
  });
}
