import { loadDevicePrivateKey } from '@chat-app/shared/auth';
import {
  type DecryptedMessage,
  editEncryptedMessage,
  parseMessagePayload,
  softDeleteMessage,
} from '@chat-app/shared/chat';
import { extractErrorCode } from '@chat-app/shared/i18n';
import { useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';

import { useAuth } from '../context/AuthContext';
import { devLocalSecretStore } from '../lib/secretStore';
import { supabase } from '../lib/supabase';
import type { AggregatedReaction } from '../lib/useMessageReactions';
import { summarizePollVotes } from '../lib/conversationFeatures';
import { extractFirstUrl } from '../lib/useLinkPreview';
import { AttachmentAudio } from './AttachmentAudio';
import { AttachmentGeneric } from './AttachmentGeneric';
import { AttachmentImage } from './AttachmentImage';
import { AttachmentPdf } from './AttachmentPdf';
import { AttachmentVideo } from './AttachmentVideo';
import { LinkPreviewCard } from './LinkPreviewCard';
import { Avatar } from './Avatar';
import {
  CopyIcon,
  ForwardIcon,
  MoreVerticalIcon,
  PencilIcon,
  PhoneIcon,
  PhoneOffIcon,
  ReplyIcon,
  SmileIcon,
  SpinnerIcon,
  TrashIcon,
  XIcon,
} from './icons';

const EMOJI_CHOICES = ['👍', '❤️', '😂', '🎉', '🔥', '😮', '😢', '🙏'];
const EDIT_WINDOW_MS = 24 * 60 * 60 * 1000;

export interface QuotedRef {
  id: string;
  senderName: string;
  snippet: string;
  isAttachment: boolean;
  deleted: boolean;
}

interface Props {
  message: DecryptedMessage;
  mine: boolean;
  groupedWithPrev: boolean;
  /** Last message of a run from this sender — anchor avatar on this row. */
  isLastOfRun?: boolean;
  senderDisplayName?: string | null | undefined;
  senderAvatarUrl?: string | null | undefined;
  conversationId: string;
  reactions: AggregatedReaction[];
  onToggleReaction: (emoji: string) => Promise<void>;
  onVotePoll?: (emoji: string, optionEmojis: string[]) => Promise<void>;
  showSeen?: boolean;
  /** Delivery state for own messages: 'sent' / 'delivered' / 'read'. */
  deliveryState?: 'sent' | 'delivered' | 'read';
  /** Resolved quoted message info (parent does the lookup). */
  quoted?: QuotedRef | null;
  /** Tap-to-jump on quote bubble. Receives the quoted message's id. */
  onJumpToMessage?: (id: string) => void;
  /** Hover action: parent receives current message to start a reply. */
  onReply?: (m: DecryptedMessage) => void;
  /** Hover action: parent opens forward dialog for current message. */
  onForward?: (m: DecryptedMessage) => void;
  /** Click on the message's avatar surfaces the author's profile card. */
  onAvatarClick?: (userId: string, ev: React.MouseEvent) => void;
  /** Highlighted state — set briefly after a jump. */
  highlighted?: boolean;
}

export function MessageBubble({
  message,
  mine,
  groupedWithPrev,
  isLastOfRun = false,
  senderDisplayName,
  senderAvatarUrl,
  conversationId,
  reactions,
  onToggleReaction,
  onVotePoll,
  showSeen = false,
  deliveryState,
  quoted = null,
  onJumpToMessage,
  onReply,
  onForward,
  onAvatarClick,
  highlighted = false,
}: Props) {
  const { t } = useTranslation(['app']);
  const { session, device } = useAuth();

  const parsed = parseMessagePayload(message.plaintext);
  const initialText = parsed.kind === 'text' ? parsed.text : '';
  const initialAttachments = parsed.kind === 'text' ? parsed.attachments : [];

  const [editing, setEditing] = useState(false);
  const [editText, setEditText] = useState(initialText);
  const [busy, setBusy] = useState(false);
  const [editError, setEditError] = useState<string | null>(null);
  const [pickerOpen, setPickerOpen] = useState(false);
  const [contextMenu, setContextMenu] = useState<{
    x: number;
    y: number;
    source: 'context' | 'more';
  } | null>(null);
  const pickerRef = useRef<HTMLDivElement>(null);

  const createdAt = new Date(message.createdAt);
  const age = Date.now() - createdAt.getTime();
  const withinEditWindow = age < EDIT_WINDOW_MS;
  const bodyText = initialText;
  const attachments = initialAttachments;
  const pollOptionEmojis =
    parsed.kind === 'poll' ? parsed.options.map((option) => option.emoji) : [];
  const visibleReactions =
    pollOptionEmojis.length === 0
      ? reactions
      : reactions.filter((reaction) => !pollOptionEmojis.includes(reaction.emoji));

  // /tempmsg ephemeral window. expireMs embedded in plaintext JSON; sender
  // fires the soft-delete when the clock runs out. Receivers just watch
  // the deletedAt flip via realtime.
  const expireMs = parsed.kind === 'text' ? parsed.expireMs : undefined;
  const [tickNow, setTickNow] = useState<number>(() => Date.now());
  useEffect(() => {
    if (expireMs === undefined) return;
    const id = window.setInterval(() => setTickNow(Date.now()), 1000);
    return () => window.clearInterval(id);
  }, [expireMs]);
  const msLeft =
    expireMs !== undefined ? Math.max(0, expireMs - (tickNow - createdAt.getTime())) : null;

  useEffect(() => {
    if (!mine) return;
    if (expireMs === undefined) return;
    if (message.deletedAt) return;
    const remaining = Math.max(0, expireMs - age);
    const timer = window.setTimeout(() => {
      void softDeleteMessage(supabase, message.id).catch((err: unknown) => {
        console.warn('ephemeral auto-delete failed', err);
      });
    }, remaining);
    return () => window.clearTimeout(timer);
  }, [mine, expireMs, age, message.id, message.deletedAt]);

  // Receiver-side auto-hide when the expiry window elapses even if the
  // sender's delete hasn't propagated yet (network hiccup, offline-sender).
  const [localExpired, setLocalExpired] = useState<boolean>(msLeft !== null && msLeft <= 0);
  useEffect(() => {
    if (expireMs === undefined) return;
    if (localExpired) return;
    const remaining = Math.max(0, expireMs - age);
    const t = window.setTimeout(() => setLocalExpired(true), remaining);
    return () => window.clearTimeout(t);
  }, [expireMs, age, localExpired]);
  const canEdit =
    parsed.kind === 'text' &&
    mine &&
    withinEditWindow &&
    !message.deletedAt &&
    attachments.length === 0;
  const canDelete =
    (parsed.kind === 'text' || parsed.kind === 'poll') && mine && !message.deletedAt;
  const canCopy = (parsed.kind === 'text' && bodyText.length > 0) || parsed.kind === 'poll';
  const hasMoreMenuActions = canCopy || canEdit || canDelete;

  const time = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(
    createdAt,
  );

  useEffect(() => {
    if (!pickerOpen) return;
    function onClickOutside(e: MouseEvent) {
      if (pickerRef.current && !pickerRef.current.contains(e.target as Node)) {
        setPickerOpen(false);
      }
    }
    document.addEventListener('mousedown', onClickOutside);
    return () => document.removeEventListener('mousedown', onClickOutside);
  }, [pickerOpen]);

  const handleEditSave = useCallback(async () => {
    if (!session || !device) return;
    const trimmed = editText.trim();
    if (!trimmed || trimmed === bodyText) {
      setEditing(false);
      setEditError(null);
      return;
    }
    setBusy(true);
    setEditError(null);
    try {
      const priv = await loadDevicePrivateKey(devLocalSecretStore, session.user.id, device.id);
      if (!priv) throw new Error('private key not loaded');
      await editEncryptedMessage({
        client: supabase,
        messageId: message.id,
        conversationId,
        newPlaintext: trimmed,
        senderUserId: session.user.id,
        senderDeviceId: device.id,
        senderPrivateKey: priv,
      });
      setEditing(false);
    } catch (err: unknown) {
      const code = extractErrorCode(err);
      setEditError(
        code
          ? t('errors:' + code, { defaultValue: t('errors:generic') })
          : err instanceof Error
            ? err.message
            : t('errors:generic'),
      );
    } finally {
      setBusy(false);
    }
  }, [editText, message.id, message.plaintext, conversationId, session, device, t]);

  const handleDelete = useCallback(async () => {
    if (busy) return;
    setBusy(true);
    try {
      await softDeleteMessage(supabase, message.id);
    } catch (err: unknown) {
      console.error('delete failed', err);
    } finally {
      setBusy(false);
    }
  }, [busy, message.id]);

  const handlePickEmoji = useCallback(
    async (emoji: string) => {
      setPickerOpen(false);
      try {
        await onToggleReaction(emoji);
      } catch (err: unknown) {
        console.error('toggleReaction failed', err);
      }
    },
    [onToggleReaction],
  );

  const copyableText =
    parsed.kind === 'poll'
      ? [parsed.question, ...parsed.options.map((option) => '- ' + option.text)].join('\n')
      : bodyText;

  const handleCopy = useCallback(async () => {
    if (!copyableText.trim()) return;
    try {
      await navigator.clipboard.writeText(copyableText);
    } catch (err: unknown) {
      console.warn('copy message failed', err);
    }
  }, [copyableText]);

  const openContextMenu = useCallback((ev: React.MouseEvent) => {
    ev.preventDefault();
    setContextMenu({ x: ev.clientX, y: ev.clientY, source: 'context' });
  }, []);

  if (message.deletedAt || localExpired) {
    return (
      <div
        className={
          '-mx-2 flex items-end gap-2 rounded-xl px-2 py-0.5 transition hover:bg-surface-2/45 dark:hover:bg-[#2e3035] ' +
          (mine ? 'flex-row-reverse' : '')
        }
      >
        <AvatarSlot
          show={isLastOfRun}
          url={senderAvatarUrl ?? null}
          displayName={senderDisplayName ?? null}
          {...(onAvatarClick
            ? { onClick: (ev: React.MouseEvent) => onAvatarClick(message.senderId, ev) }
            : {})}
        />
        <div className="max-w-[72%] rounded-2xl border border-line bg-surface-2 px-3.5 py-1.5 text-xs italic text-fg-muted dark:bg-[#383a40]">
          {t('app:chats.deleted')}
        </div>
      </div>
    );
  }

  if (parsed.kind === 'call_event') {
    return (
      <CallEventRow
        parsed={parsed}
        mine={mine}
        time={time}
        senderDisplayName={senderDisplayName ?? null}
      />
    );
  }

  return (
    <div
      onContextMenu={openContextMenu}
      className={
        '-mx-2 flex items-end gap-2 rounded-xl px-2 py-0.5 transition hover:bg-surface-2/45 dark:hover:bg-[#2e3035] ' +
        (mine ? 'flex-row-reverse' : '')
      }
    >
      <AvatarSlot
        show={isLastOfRun}
        url={senderAvatarUrl ?? null}
        displayName={senderDisplayName ?? null}
        {...(onAvatarClick
          ? { onClick: (ev: React.MouseEvent) => onAvatarClick(message.senderId, ev) }
          : {})}
      />
      <div className={'group relative max-w-[72%] ' + (groupedWithPrev ? 'mt-0.5' : 'mt-2')}>
        {editing ? (
          <div className="rounded-2xl border border-accent/40 bg-surface-2 p-2 shadow-sm dark:bg-[#383a40]">
            <textarea
              value={editText}
              onChange={(e) => setEditText(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === 'Enter' && !e.shiftKey) {
                  e.preventDefault();
                  void handleEditSave();
                }
                if (e.key === 'Escape') {
                  setEditing(false);
                  setEditError(null);
                }
              }}
              rows={2}
              autoFocus
              className="w-full resize-none rounded-md border border-line bg-surface-3 px-3 py-2 text-sm text-fg outline-none focus:ring-2 focus:ring-accent/50 dark:bg-[#313338]"
            />
            {editError && (
              <p className="mt-1.5 break-words rounded-md border border-rose-500/30 bg-rose-500/10 px-2 py-1 text-[11px] text-rose-600 dark:text-rose-200">
                {editError}
              </p>
            )}
            <div className="mt-1.5 flex justify-end gap-2">
              <button
                type="button"
                onClick={() => {
                  setEditing(false);
                  setEditError(null);
                }}
                className="cursor-pointer rounded-md border border-line bg-surface-3 px-3 py-1 text-xs text-fg hover:bg-surface-2 dark:bg-[#313338] dark:hover:bg-[#2b2d31]"
              >
                {t('app:friends.action_cancel')}
              </button>
              <button
                type="button"
                disabled={busy}
                onClick={() => void handleEditSave()}
                className="inline-flex cursor-pointer items-center gap-1.5 rounded-md bg-accent px-3 py-1 text-xs font-semibold text-accent-fg hover:brightness-110 disabled:opacity-60"
              >
                {busy && <SpinnerIcon className="h-3.5 w-3.5" />}
                <span>{t('common:save', { defaultValue: 'Save' })}</span>
              </button>
            </div>
          </div>
        ) : (
          <div
            data-message-id={message.id}
            className={
              'break-words px-3.5 py-2 text-sm shadow-sm transition ' +
              (highlighted ? 'ring-2 ring-amber-400 ring-offset-2 ring-offset-surface-3 ' : '') +
              (mine
                ? 'rounded-[18px_18px_4px_18px] bg-accent text-accent-fg'
                : 'rounded-[18px_18px_18px_4px] bg-surface-2/90 text-fg dark:bg-[#383a40]')
            }
          >
            {quoted && (
              <button
                type="button"
                onClick={() => onJumpToMessage?.(quoted.id)}
                className={
                  'mb-1.5 flex w-full cursor-pointer items-stretch gap-2 rounded-md pl-2 pr-2.5 py-1.5 text-left text-xs transition hover:brightness-110 hover:shadow-sm ' +
                  (mine
                    ? 'bg-white/10 text-accent-fg/90'
                    : 'bg-surface-3/80 text-fg-muted ring-1 ring-inset ring-line dark:bg-[#313338]')
                }
              >
                <span
                  aria-hidden="true"
                  className={
                    '-ml-1 w-1 shrink-0 rounded-full ' + (mine ? 'bg-white/70' : 'bg-accent')
                  }
                />
                <span className="min-w-0 flex-1 pl-1">
                  <span
                    className={
                      'flex items-center gap-1 truncate text-[11px] font-semibold ' +
                      (mine ? 'text-accent-fg' : 'text-accent')
                    }
                  >
                    <ReplyIcon className="h-3 w-3 shrink-0" />
                    <span className="truncate">{quoted.senderName}</span>
                  </span>
                  <span className="mt-0.5 block truncate opacity-80">
                    {quoted.deleted
                      ? t('app:chats.deleted')
                      : quoted.isAttachment && !quoted.snippet
                        ? '📎 ' + t('app:chats.attachment', { defaultValue: 'Anhang' })
                        : quoted.snippet}
                  </span>
                </span>
              </button>
            )}
            {message.plaintext === null ? (
              <span className="italic opacity-70">…cannot decrypt</span>
            ) : parsed.kind === 'poll' ? (
              <PollCard
                question={parsed.question}
                options={parsed.options}
                reactions={reactions}
                mine={mine}
                onVote={(emoji) =>
                  onVotePoll ? onVotePoll(emoji, pollOptionEmojis) : onToggleReaction(emoji)
                }
              />
            ) : (
              <>
                {bodyText.length > 0 && <div>{renderBodyWithMentions(bodyText)}</div>}
                {bodyText.length > 0 &&
                  (() => {
                    const url = extractFirstUrl(bodyText);
                    return url ? <LinkPreviewCard url={url} /> : null;
                  })()}
                {attachments.map((a) => {
                  if (a.mimeType.startsWith('audio/')) {
                    return <AttachmentAudio key={a.id} handle={a} />;
                  }
                  if (a.mimeType.startsWith('image/')) {
                    return <AttachmentImage key={a.id} handle={a} />;
                  }
                  if (a.mimeType.startsWith('video/')) {
                    return <AttachmentVideo key={a.id} handle={a} />;
                  }
                  if (a.mimeType === 'application/pdf') {
                    return <AttachmentPdf key={a.id} handle={a} />;
                  }
                  return <AttachmentGeneric key={a.id} handle={a} />;
                })}
              </>
            )}
            <div
              className={
                'mt-1 flex items-center gap-1.5 text-[10px] ' +
                (mine ? 'text-accent-fg/75' : 'text-fg-muted')
              }
            >
              <span>{time}</span>
              {message.editedAt && !message.deletedAt && (
                <span className="italic">· {t('app:chats.edited')}</span>
              )}
              {msLeft !== null && msLeft > 0 && (
                <span
                  className={
                    'inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider ' +
                    (mine ? 'bg-white/20' : 'bg-rose-500/15 text-rose-600 dark:text-rose-300')
                  }
                  title="Selbstzerstörung"
                >
                  ⏱ {Math.ceil(msLeft / 1000)}s
                </span>
              )}
            </div>
          </div>
        )}

        {mine && !editing && !message.deletedAt && deliveryState && (
          <div className="mt-0.5 flex items-center justify-end gap-1 text-[10px] text-fg-muted">
            <DeliveryTicks state={deliveryState} />
            {showSeen && <span>{t('app:chats.seen')}</span>}
          </div>
        )}

        {visibleReactions.length > 0 && !editing && (
          <div className={'mt-1 flex flex-wrap gap-1 ' + (mine ? 'justify-end' : 'justify-start')}>
            {visibleReactions.map((r) => (
              <button
                key={r.emoji + ':' + r.count}
                type="button"
                onClick={() => void onToggleReaction(r.emoji)}
                className={
                  'inline-flex origin-bottom animate-reaction-pop cursor-pointer items-center gap-1 rounded-full border px-2 py-0.5 text-xs transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
                  (r.mine
                    ? 'border-accent/40 bg-accent/20 text-accent'
                    : 'border-line bg-surface-2 text-fg hover:bg-surface-3 dark:bg-[#383a40] dark:hover:bg-[#404249]')
                }
              >
                <span>{r.emoji}</span>
                <span className="text-[10px] font-medium">{r.count}</span>
              </button>
            ))}
          </div>
        )}

        {!editing && (
          <div
            className={
              'absolute bottom-full pb-1 z-20 transition ' +
              (pickerOpen || contextMenu
                ? 'pointer-events-auto opacity-100 '
                : 'pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 ') +
              (mine ? 'right-2' : 'left-2')
            }
          >
            <div className="flex items-center gap-0.5 rounded-lg border border-line bg-surface-3 p-1 shadow-lg dark:bg-[#313338]">
              <ActionButton
                label={t('app:friends.action_accept', { defaultValue: 'React' })}
                onClick={() => setPickerOpen((v) => !v)}
                icon={<SmileIcon className="h-4 w-4" />}
              />
              {onReply && !message.deletedAt && (
                <ActionButton
                  label={t('app:chats.reply', { defaultValue: 'Antworten' })}
                  onClick={() => onReply(message)}
                  icon={<ReplyIcon className="h-4 w-4" />}
                />
              )}
              {onForward && !message.deletedAt && (
                <ActionButton
                  label={t('app:chats.forward', { defaultValue: 'Weiterleiten' })}
                  onClick={() => onForward(message)}
                  icon={<ForwardIcon className="h-4 w-4" />}
                />
              )}
              {hasMoreMenuActions && (
                <ActionButton
                  label="Mehr"
                  onClick={(ev) => {
                    const rect = ev.currentTarget.getBoundingClientRect();
                    setContextMenu({
                      x: rect.left,
                      y: rect.bottom + 8,
                      source: 'more',
                    });
                  }}
                  icon={<MoreVerticalIcon className="h-4 w-4" />}
                />
              )}
            </div>

            {pickerOpen && (
              <div
                ref={pickerRef}
                role="menu"
                className={
                  'absolute z-30 mt-1 flex gap-1 rounded-lg border border-line bg-surface-3 p-1.5 shadow-xl dark:bg-[#313338] ' +
                  (mine ? 'right-0' : 'left-0')
                }
              >
                {EMOJI_CHOICES.map((e) => (
                  <button
                    key={e}
                    type="button"
                    onClick={() => void handlePickEmoji(e)}
                    className="cursor-pointer rounded-md px-2 py-1 text-lg transition hover:bg-surface-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 dark:hover:bg-[#383a40]"
                  >
                    {e}
                  </button>
                ))}
                <button
                  type="button"
                  onClick={() => setPickerOpen(false)}
                  className="cursor-pointer rounded-md px-1.5 py-1 text-fg-muted transition hover:bg-surface-2 dark:hover:bg-[#383a40]"
                >
                  <XIcon className="h-4 w-4" />
                </button>
              </div>
            )}
          </div>
        )}

        {contextMenu && (
          <MessageContextMenu
            x={contextMenu.x}
            y={contextMenu.y}
            canCopy={canCopy}
            canEdit={canEdit}
            canDelete={canDelete}
            showQuickActions={contextMenu.source === 'context'}
            onClose={() => setContextMenu(null)}
            onReact={() => {
              setContextMenu(null);
              setPickerOpen(true);
            }}
            {...(onReply
              ? {
                  onReply: () => {
                    setContextMenu(null);
                    onReply(message);
                  },
                }
              : {})}
            {...(onForward
              ? {
                  onForward: () => {
                    setContextMenu(null);
                    onForward(message);
                  },
                }
              : {})}
            onEdit={() => {
              setContextMenu(null);
              setEditText(message.plaintext ?? '');
              setEditing(true);
            }}
            onCopy={() => {
              setContextMenu(null);
              void handleCopy();
            }}
            onDelete={() => {
              setContextMenu(null);
              void handleDelete();
            }}
          />
        )}
      </div>
    </div>
  );
}

function PollCard({
  question,
  options,
  reactions,
  onVote,
  mine,
}: {
  question: string;
  options: { id: string; emoji: string; text: string }[];
  reactions: AggregatedReaction[];
  onVote: (emoji: string) => Promise<void>;
  mine: boolean;
}) {
  const summary = summarizePollVotes(options, reactions);

  const rowBase = mine
    ? 'border-white/25 bg-white/10 hover:bg-white/15'
    : 'border-line bg-surface-3 hover:bg-surface-3/80 dark:bg-[#2b2d31] dark:hover:bg-[#313338]';
  const rowVoted = mine ? 'border-white/60 bg-white/25' : 'border-accent/60 bg-accent/20';
  const progressTint = mine ? 'bg-white/25' : 'bg-accent/25';
  const chipStyle = mine
    ? 'bg-white/20 text-accent-fg'
    : 'bg-surface-2 text-fg dark:bg-[#1e1f22]';
  const textMain = mine ? 'text-accent-fg' : 'text-fg';
  const textCount = mine ? 'text-accent-fg/90' : 'text-fg';
  const textFooter = mine ? 'text-accent-fg/75' : 'text-fg-muted';
  const focusRing = mine ? 'focus-visible:ring-white/60' : 'focus-visible:ring-accent/40';

  return (
    <div className="min-w-[260px] max-w-[360px]">
      <p className={'mb-3 text-sm font-semibold leading-snug ' + textMain}>
        {question || 'Umfrage'}
      </p>
      <div className="space-y-2">
        {summary.options.map((option, idx) => (
          <button
            key={option.id}
            type="button"
            onClick={() => void onVote(option.emoji)}
            className={
              'relative flex w-full cursor-pointer items-center gap-2.5 overflow-hidden rounded-lg border px-3 py-2 text-left text-sm transition focus:outline-none focus-visible:ring-2 ' +
              focusRing +
              ' ' +
              textMain +
              ' ' +
              (option.mine ? rowVoted : rowBase)
            }
          >
            <span
              aria-hidden="true"
              style={{ width: option.percent + '%' }}
              className={'absolute inset-y-0 left-0 transition-[width] ' + progressTint}
            />
            <span
              aria-hidden="true"
              className={
                'relative flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-sm font-bold tabular-nums ' +
                chipStyle
              }
            >
              {idx + 1}
            </span>
            <span className="relative min-w-0 flex-1 break-words font-medium">{option.text}</span>
            <span className={'relative shrink-0 text-xs font-semibold tabular-nums ' + textCount}>
              {option.count} · {option.percent}%
            </span>
          </button>
        ))}
      </div>
      <p className={'mt-2 text-[11px] ' + textFooter}>
        {summary.totalVotes === 1 ? '1 Stimme' : summary.totalVotes + ' Stimmen'}
      </p>
    </div>
  );
}

function MessageContextMenu({
  x,
  y,
  canCopy,
  canEdit,
  canDelete,
  showQuickActions,
  onClose,
  onReact,
  onReply,
  onForward,
  onEdit,
  onCopy,
  onDelete,
}: {
  x: number;
  y: number;
  canCopy: boolean;
  canEdit: boolean;
  canDelete: boolean;
  showQuickActions: boolean;
  onClose: () => void;
  onReact: () => void;
  onReply?: () => void;
  onForward?: () => void;
  onEdit: () => void;
  onCopy: () => void;
  onDelete: () => void;
}) {
  useEffect(() => {
    function onKey(e: KeyboardEvent) {
      if (e.key === 'Escape') onClose();
    }
    function onDown(e: MouseEvent) {
      const target = e.target as HTMLElement | null;
      if (target?.closest('[data-message-context-menu]')) return;
      onClose();
    }
    window.addEventListener('keydown', onKey);
    window.addEventListener('mousedown', onDown);
    return () => {
      window.removeEventListener('keydown', onKey);
      window.removeEventListener('mousedown', onDown);
    };
  }, [onClose]);

  const left = Math.min(Math.max(8, x), window.innerWidth - 232);
  const top = Math.min(Math.max(8, y), window.innerHeight - 280);

  return createPortal(
    <div
      data-message-context-menu
      role="menu"
      style={{ left, top, width: 224 }}
      className="fixed z-[90] rounded-xl border border-line bg-surface-3 p-1.5 shadow-2xl dark:bg-[#313338]"
    >
      {showQuickActions && (
        <>
          <MenuItem
            label="Reaktion hinzufügen"
            icon={<SmileIcon className="h-4 w-4" />}
            onClick={onReact}
          />
          {onReply && (
            <MenuItem
              label="Antworten"
              icon={<ReplyIcon className="h-4 w-4" />}
              onClick={onReply}
            />
          )}
          {onForward && (
            <MenuItem
              label="Weiterleiten"
              icon={<ForwardIcon className="h-4 w-4" />}
              onClick={onForward}
            />
          )}
          {(canCopy || canEdit || canDelete) && <MenuSeparator />}
        </>
      )}
      {canCopy && (
        <MenuItem label="Text kopieren" icon={<CopyIcon className="h-4 w-4" />} onClick={onCopy} />
      )}
      {canEdit && (
        <MenuItem label="Bearbeiten" icon={<PencilIcon className="h-4 w-4" />} onClick={onEdit} />
      )}
      {canDelete && (
        <MenuItem
          label="Löschen"
          icon={<TrashIcon className="h-4 w-4" />}
          onClick={onDelete}
          tone="danger"
        />
      )}
    </div>,
    document.body,
  );
}

function MenuSeparator() {
  return <div aria-hidden="true" className="my-1 h-px bg-line" />;
}

function MenuItem({
  label,
  icon,
  onClick,
  tone,
}: {
  label: string;
  icon: React.ReactNode;
  onClick: () => void;
  tone?: 'danger';
}) {
  return (
    <button
      type="button"
      role="menuitem"
      onClick={onClick}
      className={
        'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
        (tone === 'danger'
          ? 'text-rose-500 hover:bg-rose-500/15 dark:text-rose-300'
          : 'text-fg hover:bg-surface-2 dark:hover:bg-[#383a40]')
      }
    >
      <span className="shrink-0 text-fg-muted">{icon}</span>
      <span className="min-w-0 flex-1 truncate">{label}</span>
    </button>
  );
}

function AvatarSlot({
  show,
  url,
  displayName,
  onClick,
}: {
  show: boolean;
  url: string | null;
  displayName: string | null;
  onClick?: (ev: React.MouseEvent) => void;
}) {
  if (!show) {
    return <div aria-hidden="true" className="h-8 w-8 shrink-0" />;
  }
  if (onClick) {
    return (
      <button
        type="button"
        data-user-popover-trigger
        onClick={onClick}
        className="shrink-0 cursor-pointer rounded-full transition hover:ring-2 hover:ring-accent/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
        aria-label={displayName ?? 'Profil'}
      >
        <Avatar url={url} displayName={displayName ?? ''} className="h-8 w-8 text-xs" />
      </button>
    );
  }
  return <Avatar url={url} displayName={displayName ?? ''} className="h-8 w-8 text-xs" />;
}

function CallEventRow({
  parsed,
  mine,
  time,
  senderDisplayName,
}: {
  parsed: { status: string; mediaKind: string; durationSec: number };
  mine: boolean;
  time: string;
  /** Discord-parity: in group chats the system pill should say WHO
   *  started/missed the call. Null means we don't know (fall back to the
   *  legacy generic labels). */
  senderDisplayName: string | null;
}) {
  const { t } = useTranslation(['app']);
  const status = parsed.status;
  const isMissed = status === 'missed' || status === 'declined';

  const Icon = isMissed ? PhoneOffIcon : PhoneIcon;
  const tone = isMissed
    ? 'border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-200'
    : 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-200';

  // For non-own events we prefer the name-aware label so group chats
  // make it clear who triggered the call event. Own events stay generic
  // ("Outgoing call" / "No answer") since the user already knows they
  // were the initiator.
  const hasName = !mine && !!senderDisplayName;
  const label =
    status === 'ended'
      ? mine
        ? t('app:chats.call_outgoing', { defaultValue: 'Outgoing call' })
        : hasName
          ? t('app:chats.call_started_by', {
              name: senderDisplayName,
              defaultValue: '{{name}} hat einen Anruf gestartet',
            })
          : t('app:chats.call_incoming', { defaultValue: 'Incoming call' })
      : status === 'missed'
        ? mine
          ? t('app:chats.call_no_answer', { defaultValue: 'No answer' })
          : hasName
            ? t('app:chats.call_missed_by', {
                name: senderDisplayName,
                defaultValue: 'Verpasster Anruf von {{name}}',
              })
            : t('app:chats.call_missed', { defaultValue: 'Missed call' })
        : hasName
          ? t('app:chats.call_declined_by', {
              name: senderDisplayName,
              defaultValue: 'Anruf von {{name}} abgelehnt',
            })
          : t('app:chats.call_declined', { defaultValue: 'Call declined' });

  const duration = parsed.durationSec > 0 ? formatDuration(parsed.durationSec) : null;

  return (
    <div className="my-2 flex justify-center">
      <div
        className={
          'inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium ' + tone
        }
      >
        <Icon className="h-3.5 w-3.5" />
        <span>{label}</span>
        {duration && <span className="font-mono text-[11px] opacity-80">· {duration}</span>}
        <span className="text-[10px] opacity-60">· {time}</span>
      </div>
    </div>
  );
}

function formatDuration(totalSec: number): string {
  const m = Math.floor(totalSec / 60);
  const s = totalSec % 60;
  if (m === 0) return s + 's';
  return m + ':' + s.toString().padStart(2, '0');
}

const MENTION_RE = /@([A-Za-z0-9_]{1,32})/g;

function renderBodyWithMentions(text: string): React.ReactNode[] {
  const out: React.ReactNode[] = [];
  let lastIdx = 0;
  let m: RegExpExecArray | null;
  MENTION_RE.lastIndex = 0;
  while ((m = MENTION_RE.exec(text)) !== null) {
    if (m.index > lastIdx) out.push(text.slice(lastIdx, m.index));
    out.push(
      <span
        key={m.index + ':' + m[1]}
        className="rounded bg-accent/20 px-1 font-medium text-inherit ring-1 ring-inset ring-accent/25"
      >
        {m[0]}
      </span>,
    );
    lastIdx = m.index + m[0].length;
  }
  if (lastIdx < text.length) out.push(text.slice(lastIdx));
  return out;
}

function DeliveryTicks({ state }: { state: 'sent' | 'delivered' | 'read' }) {
  const color = state === 'read' ? 'text-sky-500 dark:text-sky-400' : 'text-fg-muted';
  return (
    <span
      aria-label={state === 'read' ? 'Gelesen' : state === 'delivered' ? 'Zugestellt' : 'Gesendet'}
      title={state === 'read' ? 'Gelesen' : state === 'delivered' ? 'Zugestellt' : 'Gesendet'}
      className={'flex items-center ' + color}
    >
      <svg
        viewBox="0 0 16 12"
        width="14"
        height="10"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.5"
        strokeLinecap="round"
        strokeLinejoin="round"
        aria-hidden="true"
      >
        {state === 'sent' ? (
          <polyline points="2 7 6 11 14 1" />
        ) : (
          <>
            <polyline points="1 7 5 11 11 2" />
            <polyline points="6 11 10 11 14 1" />
          </>
        )}
      </svg>
    </span>
  );
}

function ActionButton({
  label,
  onClick,
  icon,
  tone,
}: {
  label: string;
  onClick: (ev: React.MouseEvent<HTMLButtonElement>) => void;
  icon: React.ReactNode;
  tone?: 'danger';
}) {
  return (
    <button
      type="button"
      aria-label={label}
      title={label}
      onClick={onClick}
      className={
        'flex h-7 w-7 cursor-pointer items-center justify-center rounded-md transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
        (tone === 'danger'
          ? 'text-fg-muted hover:bg-rose-500/20 hover:text-rose-500 dark:hover:text-rose-200'
          : 'text-fg-muted hover:bg-surface-2 hover:text-fg dark:hover:bg-[#383a40]')
      }
    >
      {icon}
    </button>
  );
}
