import { parseMessagePayload } from '@chat-app/shared/chat';
import { extractErrorCode } from '@chat-app/shared/i18n';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';

import { ConversationHeader } from '../components/ConversationHeader';
import { ForwardDialog } from '../components/ForwardDialog';
import { GroupInfoPanel } from '../components/GroupInfoPanel';
import {
  AlertIcon,
  ArrowRightIcon,
  ChevronDownIcon,
  ChevronUpIcon,
  PlusIcon,
  ReplyIcon,
  SearchIcon,
  SpinnerIcon,
  XIcon,
} from '../components/icons';
import { InCallPanel } from '../components/InCallPanel';
import { IncomingCallPanel } from '../components/IncomingCallPanel';
import { MessageBubble, type QuotedRef } from '../components/MessageBubble';
import { TypingIndicator } from '../components/TypingIndicator';
import type { DecryptedMessage } from '@chat-app/shared/chat';
import { useAuth } from '../context/AuthContext';
import { useCall } from '../context/CallContext';
import { useConversationsContext } from '../context/ConversationsContext';
import { useConversationMessages } from '../lib/useConversationMessages';
import { useMessageReactions } from '../lib/useMessageReactions';
import { markRead as markMessagesReadRemote, useMessageReads } from '../lib/useMessageReads';
import { usePeerPresence } from '../lib/usePeerPresence';
import { useTypingChannel } from '../lib/useTypingChannel';

const STICK_THRESHOLD = 80;

export function ConversationPage() {
  const { t } = useTranslation(['app', 'errors']);
  const { id } = useParams<{ id: string }>();
  const { session, device } = useAuth();
  const { conversations, setActiveConversation, markRead } = useConversationsContext();

  const conversation = useMemo(
    () => conversations.find((c) => c.id === id) ?? null,
    [conversations, id],
  );
  const peerId = conversation?.peer?.userId;
  const peerPresence = usePeerPresence(peerId);

  const { messages, loading, error, send } = useConversationMessages({
    conversationId: id,
    userId: session?.user.id,
    deviceId: device?.id,
  });
  const messageIds = useMemo(() => messages.map((m) => m.id), [messages]);
  const { byMessage: reactionsByMessage, toggle: toggleReaction } = useMessageReactions(
    messageIds,
    session?.user.id,
  );

  const myId = session?.user.id;

  // Peer read tracking — only for 1:1 DMs.
  const ownMessageIds = useMemo(
    () => messages.filter((m) => m.senderId === myId).map((m) => m.id),
    [messages, myId],
  );
  const { peerReadSet } = useMessageReads(ownMessageIds, peerId);

  const lastSeenMessageId = useMemo(() => {
    for (let i = messages.length - 1; i >= 0; i--) {
      const m = messages[i];
      if (m && m.senderId === myId && peerReadSet.has(m.id)) return m.id;
    }
    return null;
  }, [messages, peerReadSet, myId]);

  // Typing channel.
  const { typingUserIds, notifyTyping, notifyStopTyping } = useTypingChannel(id, myId);

  // Mark incoming messages as read (server-side, visible to peer if both sides
  // have receipts on). Runs whenever new messages arrive or id changes.
  useEffect(() => {
    if (!id || messages.length === 0 || !myId) return;
    const incoming = messages.filter((m) => m.senderId !== myId).map((m) => m.id);
    if (incoming.length === 0) return;
    void markMessagesReadRemote(incoming).catch((err: unknown) => {
      console.error('markMessagesRead failed', err);
    });
  }, [id, messages, myId]);

  const [text, setText] = useState('');
  const [sending, setSending] = useState(false);
  const [sendError, setSendError] = useState<string | null>(null);
  const [stickToBottom, setStickToBottom] = useState(true);
  const [attachments, setAttachments] = useState<File[]>([]);
  const [infoPanelOpen, setInfoPanelOpen] = useState(false);
  const [replyTo, setReplyTo] = useState<DecryptedMessage | null>(null);
  const [forwardTarget, setForwardTarget] = useState<DecryptedMessage | null>(null);
  const [searchOpen, setSearchOpen] = useState(false);
  const [searchQuery, setSearchQuery] = useState('');
  const [searchIdx, setSearchIdx] = useState(0);
  const [highlightedId, setHighlightedId] = useState<string | null>(null);
  const scrollRef = useRef<HTMLDivElement>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);
  const composerRef = useRef<HTMLTextAreaElement>(null);

  // Drop reply-to / clear search state when switching conversation.
  useEffect(() => {
    setReplyTo(null);
    setForwardTarget(null);
    setSearchOpen(false);
    setSearchQuery('');
  }, [id]);

  const messageById = useMemo(() => {
    const m = new Map<string, DecryptedMessage>();
    for (const msg of messages) m.set(msg.id, msg);
    return m;
  }, [messages]);

  const senderNameFor = useCallback(
    (senderId: string): string => {
      if (senderId === myId) return t('app:chats.you', { defaultValue: 'Du' });
      const profile =
        conversation?.members.find((mm) => mm.userId === senderId)?.profile ??
        (senderId !== myId ? conversation?.peer ?? null : null);
      return profile?.displayName ?? '?';
    },
    [conversation, myId, t],
  );

  const buildQuoted = useCallback(
    (replyToId: string | null): QuotedRef | null => {
      if (!replyToId) return null;
      const target = messageById.get(replyToId);
      if (!target) {
        return {
          id: replyToId,
          senderName: '…',
          snippet: t('app:chats.quote_unavailable', { defaultValue: 'Nachricht nicht verfügbar' }),
          isAttachment: false,
          deleted: true,
        };
      }
      const parsed = parseMessagePayload(target.plaintext);
      const text = parsed.kind === 'text' ? parsed.text : '';
      const hasAttachment = parsed.kind === 'text' && parsed.attachments.length > 0;
      return {
        id: target.id,
        senderName: senderNameFor(target.senderId),
        snippet: text.length > 120 ? text.slice(0, 120) + '…' : text,
        isAttachment: hasAttachment,
        deleted: !!target.deletedAt,
      };
    },
    [messageById, senderNameFor, t],
  );

  const jumpToMessage = useCallback((targetId: string) => {
    const el = scrollRef.current?.querySelector<HTMLElement>(
      '[data-message-id="' + CSS.escape(targetId) + '"]',
    );
    if (!el) return;
    el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    setHighlightedId(targetId);
    window.setTimeout(() => setHighlightedId((cur) => (cur === targetId ? null : cur)), 1600);
  }, []);

  const handleReply = useCallback((m: DecryptedMessage) => {
    setReplyTo(m);
    composerRef.current?.focus();
  }, []);

  const handleForward = useCallback((m: DecryptedMessage) => {
    setForwardTarget(m);
  }, []);

  // Search matches: messages whose decrypted text includes the query.
  const searchMatches = useMemo(() => {
    const q = searchQuery.trim().toLowerCase();
    if (!q) return [] as DecryptedMessage[];
    return messages.filter((m) => {
      if (!m.plaintext) return false;
      const parsed = parseMessagePayload(m.plaintext);
      if (parsed.kind !== 'text') return false;
      return parsed.text.toLowerCase().includes(q);
    });
  }, [messages, searchQuery]);

  // Reset/clamp the active match index when the match set changes.
  useEffect(() => {
    if (searchMatches.length === 0) {
      setSearchIdx(0);
      return;
    }
    setSearchIdx((cur) => Math.min(cur, searchMatches.length - 1));
  }, [searchMatches.length]);

  // Auto-jump to current match.
  useEffect(() => {
    if (!searchOpen || searchMatches.length === 0) return;
    const target = searchMatches[searchIdx];
    if (target) jumpToMessage(target.id);
  }, [searchOpen, searchMatches, searchIdx, jumpToMessage]);

  useEffect(() => {
    if (!id) return;
    setActiveConversation(id);
    return () => {
      setActiveConversation(null);
    };
  }, [id, setActiveConversation]);

  useEffect(() => {
    if (id && messages.length > 0) markRead(id);
  }, [id, messages.length, markRead]);

  useEffect(() => {
    const el = scrollRef.current;
    if (!el || !stickToBottom) return;
    el.scrollTop = el.scrollHeight;
  }, [messages.length, stickToBottom]);

  useEffect(() => {
    setStickToBottom(true);
    const el = scrollRef.current;
    if (el) el.scrollTop = el.scrollHeight;
  }, [id]);

  const handleScroll = useCallback(() => {
    const el = scrollRef.current;
    if (!el) return;
    const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
    setStickToBottom(distanceFromBottom < STICK_THRESHOLD);
  }, []);

  async function handleSend(e?: React.FormEvent) {
    e?.preventDefault();
    if ((!text.trim() && attachments.length === 0) || sending) return;
    setSending(true);
    setSendError(null);
    try {
      await send(text, attachments, replyTo?.id ?? null);
      setText('');
      setAttachments([]);
      setReplyTo(null);
      if (fileInputRef.current) fileInputRef.current.value = '';
      setStickToBottom(true);
      notifyStopTyping();
    } catch (err: unknown) {
      const code = extractErrorCode(err);
      setSendError(
        code
          ? t('errors:' + code, { defaultValue: t('errors:generic') })
          : err instanceof Error
            ? err.message
            : t('errors:generic'),
      );
    } finally {
      setSending(false);
    }
  }

  function handleFilesChosen(list: FileList | null) {
    if (!list) return;
    const next: File[] = [];
    for (let i = 0; i < list.length; i++) {
      const f = list[i];
      if (!f) continue;
      if (!f.type.startsWith('image/')) continue;
      if (f.size > 10 * 1024 * 1024) {
        setSendError('Datei zu groß (max 10 MB)');
        continue;
      }
      next.push(f);
    }
    setAttachments((prev) => [...prev, ...next].slice(0, 4));
  }

  const isGroup = conversation?.type === 'group';
  const { state: callState } = useCall();
  // Hide the chat header while this conversation hosts an active call — the
  // call topbar inside the dock already shows the channel name + duration,
  // and fullscreen cinema needs the whole slot.
  const callHereActive =
    (callState.kind === 'connected' ||
      callState.kind === 'connecting' ||
      callState.kind === 'outgoing') &&
    callState.conversationId === id;
  const incomingHere =
    callState.kind === 'incoming' && callState.conversationId === id;

  return (
    <div className="relative flex h-full flex-col">
      {!callHereActive && (
        <ConversationHeader
          conversation={conversation}
          peerPresence={peerPresence}
          onSearchClick={() => setSearchOpen((v) => !v)}
          {...(isGroup ? { onInfoClick: () => setInfoPanelOpen(true) } : {})}
        />
      )}

      {!callHereActive && searchOpen && (
        <SearchBar
          query={searchQuery}
          onQueryChange={setSearchQuery}
          matches={searchMatches.length}
          activeIdx={searchIdx}
          onPrev={() =>
            setSearchIdx((cur) =>
              searchMatches.length === 0 ? 0 : (cur - 1 + searchMatches.length) % searchMatches.length,
            )
          }
          onNext={() =>
            setSearchIdx((cur) => (searchMatches.length === 0 ? 0 : (cur + 1) % searchMatches.length))
          }
          onClose={() => {
            setSearchOpen(false);
            setSearchQuery('');
          }}
        />
      )}

      {isGroup && conversation && (
        <GroupInfoPanel
          open={infoPanelOpen}
          onClose={() => setInfoPanelOpen(false)}
          conversation={conversation}
        />
      )}

      {incomingHere && conversation && <IncomingCallPanel conversation={conversation} />}
      {conversation && <InCallPanel conversation={conversation} />}

      <div ref={scrollRef} onScroll={handleScroll} className="flex-1 overflow-y-auto bg-surface-3 px-6 py-4">
        {loading ? (
          <div className="flex items-center gap-2 text-xs text-fg-muted">
            <SpinnerIcon className="h-3.5 w-3.5 text-accent" />
          </div>
        ) : error ? (
          <Banner>{error}</Banner>
        ) : messages.length === 0 ? (
          <p className="text-center text-sm text-fg-muted">…</p>
        ) : (
          <ul className="space-y-0.5">
            {messages.map((m, idx) => {
              // A "run" is consecutive bubbles from the same sender with
              // nothing between them. Call-event separators break the run —
              // a bubble whose immediate next neighbour is a call_event must
              // anchor the avatar, even if another bubble from the same
              // sender appears after the separator.
              const prevRaw = messages[idx - 1];
              const nextRaw = messages[idx + 1];
              const prevIsCallEvent =
                !!prevRaw && parseMessagePayload(prevRaw.plaintext).kind === 'call_event';
              const nextIsCallEvent =
                !!nextRaw && parseMessagePayload(nextRaw.plaintext).kind === 'call_event';
              const grouped =
                !!prevRaw && prevRaw.senderId === m.senderId && !prevIsCallEvent;
              // Anchor avatar on the LAST message of a run so it aligns with
              // the bubble's tail (bottom corner). Tail is bottom-left for
              // mine, bottom-right for peer — see rounded-[…_4px_…] above.
              const isLastOfRun =
                !nextRaw || nextRaw.senderId !== m.senderId || nextIsCallEvent;
              // DM fallback: if member lookup fails (e.g. transient sync), fall
              // back to conversation.peer so the peer's avatar still resolves.
              const memberProfile =
                conversation?.members.find((mm) => mm.userId === m.senderId)?.profile ?? null;
              const senderProfile =
                memberProfile ??
                (m.senderId !== myId ? (conversation?.peer ?? null) : null);
              return (
                <li key={m.id}>
                  <MessageBubble
                    message={m}
                    mine={m.senderId === myId}
                    groupedWithPrev={grouped}
                    isLastOfRun={isLastOfRun}
                    senderDisplayName={senderProfile?.displayName}
                    senderAvatarUrl={senderProfile?.avatarUrl}
                    conversationId={id ?? ''}
                    reactions={reactionsByMessage.get(m.id) ?? []}
                    onToggleReaction={(emoji) => toggleReaction(m.id, emoji)}
                    showSeen={m.id === lastSeenMessageId}
                    quoted={buildQuoted(m.replyToId)}
                    onJumpToMessage={jumpToMessage}
                    onReply={handleReply}
                    onForward={handleForward}
                    highlighted={highlightedId === m.id}
                  />
                </li>
              );
            })}
          </ul>
        )}
      </div>

      <TypingIndicator
        typingUserIds={typingUserIds}
        members={conversation?.members ?? []}
      />

      <form onSubmit={handleSend} className="border-t border-line bg-surface-3 p-4">
        {sendError && (
          <div className="mb-2">
            <Banner>{sendError}</Banner>
          </div>
        )}

        {replyTo && (
          <div className="mb-2 flex items-stretch gap-2 rounded-lg border border-line bg-surface-2 p-2 text-sm">
            <span aria-hidden="true" className="w-1 shrink-0 rounded-full bg-accent" />
            <ReplyIcon className="mt-0.5 h-4 w-4 shrink-0 text-accent" />
            <div className="min-w-0 flex-1">
              <p className="truncate text-xs font-semibold text-fg">
                {t('app:chats.replying_to', {
                  name: senderNameFor(replyTo.senderId),
                  defaultValue: 'Antwort an ' + senderNameFor(replyTo.senderId),
                })}
              </p>
              <p className="truncate text-xs italic text-fg-muted">
                {(() => {
                  if (!replyTo.plaintext) return '…';
                  const p = parseMessagePayload(replyTo.plaintext);
                  if (p.kind !== 'text') return '';
                  if (!p.text && p.attachments.length > 0) return '📎';
                  return p.text;
                })()}
              </p>
            </div>
            <button
              type="button"
              onClick={() => setReplyTo(null)}
              aria-label={t('app:friends.action_cancel', { defaultValue: 'Abbrechen' })}
              className="shrink-0 cursor-pointer rounded-md p-1 text-fg-muted transition hover:bg-surface-3 hover:text-fg"
            >
              <XIcon className="h-4 w-4" />
            </button>
          </div>
        )}

        {attachments.length > 0 && (
          <div className="mb-2 flex flex-wrap gap-2">
            {attachments.map((file, idx) => (
              <AttachmentPreview
                key={idx}
                file={file}
                onRemove={() =>
                  setAttachments((prev) => prev.filter((_, i) => i !== idx))
                }
              />
            ))}
          </div>
        )}

        <div className="flex items-end gap-2">
          <input
            ref={fileInputRef}
            type="file"
            accept="image/*"
            multiple
            className="hidden"
            onChange={(e) => handleFilesChosen(e.target.files)}
          />
          <button
            type="button"
            onClick={() => fileInputRef.current?.click()}
            aria-label="Bild anhängen"
            title="Bild anhängen"
            className="inline-flex h-11 w-11 shrink-0 cursor-pointer items-center justify-center rounded-lg bg-accent text-accent-fg transition hover:brightness-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
          >
            <PlusIcon className="h-4 w-4" />
          </button>
          <textarea
            ref={composerRef}
            value={text}
            onChange={(e) => {
              setText(e.target.value);
              if (e.target.value.length > 0) notifyTyping();
            }}
            onKeyDown={(e) => {
              if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                void handleSend();
              }
            }}
            rows={1}
            placeholder="Nachricht schreiben…"
            className="max-h-40 min-h-[44px] flex-1 resize-none rounded-lg border border-line bg-surface-2 px-3 py-2.5 text-sm text-fg placeholder-fg-muted transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30"
          />
          <button
            type="submit"
            disabled={sending || (text.trim().length === 0 && attachments.length === 0)}
            aria-busy={sending}
            className="inline-flex h-11 w-11 shrink-0 cursor-pointer items-center justify-center rounded-lg bg-accent text-accent-fg transition hover:brightness-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 disabled:cursor-not-allowed disabled:opacity-60"
          >
            {sending ? <SpinnerIcon className="h-4 w-4" /> : <ArrowRightIcon className="h-4 w-4" />}
          </button>
        </div>
      </form>

      <ForwardDialog
        open={forwardTarget !== null}
        message={forwardTarget}
        currentConversationId={id ?? null}
        onClose={() => setForwardTarget(null)}
      />
    </div>
  );
}

interface SearchBarProps {
  query: string;
  onQueryChange: (q: string) => void;
  matches: number;
  activeIdx: number;
  onPrev: () => void;
  onNext: () => void;
  onClose: () => void;
}

function SearchBar({ query, onQueryChange, matches, activeIdx, onPrev, onNext, onClose }: SearchBarProps) {
  const { t } = useTranslation(['app']);
  return (
    <div className="flex items-center gap-2 border-b border-line bg-surface-2 px-4 py-2">
      <SearchIcon className="h-4 w-4 shrink-0 text-fg-muted" />
      <input
        type="search"
        autoFocus
        value={query}
        onChange={(e) => onQueryChange(e.target.value)}
        onKeyDown={(e) => {
          if (e.key === 'Escape') onClose();
          if (e.key === 'Enter') {
            e.preventDefault();
            if (e.shiftKey) onPrev();
            else onNext();
          }
        }}
        placeholder={t('app:chats.search_in_conv', { defaultValue: 'In Unterhaltung suchen…' })}
        className="min-w-0 flex-1 bg-transparent text-sm text-fg placeholder-fg-muted outline-none"
      />
      <span className="shrink-0 text-xs tabular-nums text-fg-muted">
        {matches === 0
          ? query.trim().length > 0
            ? t('app:chats.search_none', { defaultValue: 'Keine Treffer' })
            : ''
          : activeIdx + 1 + ' / ' + matches}
      </span>
      <button
        type="button"
        onClick={onPrev}
        disabled={matches === 0}
        aria-label="Previous"
        className="cursor-pointer rounded-md p-1 text-fg-muted transition hover:bg-surface-3 hover:text-fg disabled:cursor-not-allowed disabled:opacity-40"
      >
        <ChevronUpIcon className="h-4 w-4" />
      </button>
      <button
        type="button"
        onClick={onNext}
        disabled={matches === 0}
        aria-label="Next"
        className="cursor-pointer rounded-md p-1 text-fg-muted transition hover:bg-surface-3 hover:text-fg disabled:cursor-not-allowed disabled:opacity-40"
      >
        <ChevronDownIcon className="h-4 w-4" />
      </button>
      <button
        type="button"
        onClick={onClose}
        aria-label="Close"
        className="cursor-pointer rounded-md p-1 text-fg-muted transition hover:bg-surface-3 hover:text-fg"
      >
        <XIcon className="h-4 w-4" />
      </button>
    </div>
  );
}

// Walks `messages` from `idx + step` skipping call_event entries until a
// regular bubble is found or the array boundary is reached. Used to decide
// run-grouping for avatar placement so call separators don't bleed into
// sender continuity.
function Banner({ children }: { children: React.ReactNode }) {
  return (
    <div
      role="alert"
      className="flex items-start gap-3 rounded-lg border border-rose-500/30 bg-rose-500/10 p-3 text-sm text-rose-700 dark:text-rose-100"
    >
      <AlertIcon className="mt-0.5 h-5 w-5 shrink-0 text-rose-500 dark:text-rose-400" />
      <p className="min-w-0 flex-1 break-words">{children}</p>
    </div>
  );
}

function AttachmentPreview({ file, onRemove }: { file: File; onRemove: () => void }) {
  const [url, setUrl] = useState<string | null>(null);
  useEffect(() => {
    const u = URL.createObjectURL(file);
    setUrl(u);
    return () => URL.revokeObjectURL(u);
  }, [file]);
  return (
    <div className="relative overflow-hidden rounded-lg border border-line bg-surface-2">
      {url ? (
        <img src={url} alt={file.name} className="block h-20 w-20 object-cover" />
      ) : (
        <div className="h-20 w-20" />
      )}
      <button
        type="button"
        onClick={onRemove}
        aria-label="Entfernen"
        className="absolute right-1 top-1 flex h-5 w-5 cursor-pointer items-center justify-center rounded-full bg-black/70 text-white transition hover:bg-rose-500/80"
      >
        <XIcon className="h-3 w-3" />
      </button>
    </div>
  );
}
