import { useMemo, useState, memo, useCallback, useRef, useEffect } from "react";
import { use$ } from "applesauce-react/hooks";
import { from, catchError, of, map } from "rxjs";
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
import {
  Loader2,
  Reply,
  Zap,
  AlertTriangle,
  RefreshCw,
  Paperclip,
  Copy,
  CopyCheck,
} from "lucide-react";
import { nip19 } from "nostr-tools";
import { getZapRequest } from "applesauce-common/helpers/zap";
import { toast } from "sonner";
import eventStore from "@/services/event-store";
import type {
  ChatProtocol,
  ProtocolIdentifier,
  Conversation,
  LiveActivityMetadata,
} from "@/types/chat";
import { CHAT_KINDS } from "@/types/chat";
// import { NipC7Adapter } from "@/lib/chat/adapters/nip-c7-adapter";  // Coming soon
import { Nip29Adapter } from "@/lib/chat/adapters/nip-29-adapter";
import { Nip53Adapter } from "@/lib/chat/adapters/nip-53-adapter";
import type { ChatProtocolAdapter } from "@/lib/chat/adapters/base-adapter";
import type { Message } from "@/types/chat";
import type { ChatAction } from "@/types/chat-actions";
import { parseSlashCommand } from "@/lib/chat/slash-command-parser";
import { UserName } from "./nostr/UserName";
import { RichText } from "./nostr/RichText";
import Timestamp from "./Timestamp";
import { ReplyPreview } from "./chat/ReplyPreview";
import { MembersDropdown } from "./chat/MembersDropdown";
import { RelaysDropdown } from "./chat/RelaysDropdown";
import { MessageReactions } from "./chat/MessageReactions";
import { StatusBadge } from "./live/StatusBadge";
import { ChatMessageContextMenu } from "./chat/ChatMessageContextMenu";
import { useGrimoire } from "@/core/state";
import { Button } from "./ui/button";
import {
  MentionEditor,
  type MentionEditorHandle,
  type EmojiTag,
  type BlobAttachment,
} from "./editor/MentionEditor";
import { useProfileSearch } from "@/hooks/useProfileSearch";
import { useEmojiSearch } from "@/hooks/useEmojiSearch";
import { useCopy } from "@/hooks/useCopy";
import { useAccount } from "@/hooks/useAccount";
import { Label } from "./ui/label";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "./ui/tooltip";
import { useBlossomUpload } from "@/hooks/useBlossomUpload";

interface ChatViewerProps {
  protocol: ChatProtocol;
  identifier: ProtocolIdentifier;
  customTitle?: string;
  /** Optional content to render before the title (e.g., sidebar toggle on mobile) */
  headerPrefix?: React.ReactNode;
}

/**
 * Helper: Format timestamp as a readable day marker
 */
function formatDayMarker(timestamp: number): string {
  const date = new Date(timestamp * 1000);
  const today = new Date();
  const yesterday = new Date(today);
  yesterday.setDate(yesterday.getDate() - 1);

  // Reset time parts for comparison
  const dateOnly = new Date(
    date.getFullYear(),
    date.getMonth(),
    date.getDate(),
  );
  const todayOnly = new Date(
    today.getFullYear(),
    today.getMonth(),
    today.getDate(),
  );
  const yesterdayOnly = new Date(
    yesterday.getFullYear(),
    yesterday.getMonth(),
    yesterday.getDate(),
  );

  if (dateOnly.getTime() === todayOnly.getTime()) {
    return "Today";
  } else if (dateOnly.getTime() === yesterdayOnly.getTime()) {
    return "Yesterday";
  } else {
    // Format as "Jan 15" (short month, no year, respects locale)
    return date.toLocaleDateString(undefined, {
      month: "short",
      day: "numeric",
    });
  }
}

/**
 * Helper: Check if two timestamps are on different days
 */
function isDifferentDay(timestamp1: number, timestamp2: number): boolean {
  const date1 = new Date(timestamp1 * 1000);
  const date2 = new Date(timestamp2 * 1000);

  return (
    date1.getFullYear() !== date2.getFullYear() ||
    date1.getMonth() !== date2.getMonth() ||
    date1.getDate() !== date2.getDate()
  );
}

/**
 * Type guard for LiveActivityMetadata
 */
function isLiveActivityMetadata(value: unknown): value is LiveActivityMetadata {
  if (!value || typeof value !== "object") return false;
  const obj = value as Record<string, unknown>;
  return (
    typeof obj.status === "string" &&
    typeof obj.hostPubkey === "string" &&
    Array.isArray(obj.hashtags) &&
    Array.isArray(obj.relays)
  );
}

/**
 * Get relay URLs for a conversation based on protocol
 * Used for fetching protocol-specific data like reactions
 */
function getConversationRelays(conversation: Conversation): string[] {
  // NIP-53 live chats: Use full relay list from liveActivity metadata
  if (conversation.protocol === "nip-53") {
    const liveActivity = conversation.metadata?.liveActivity;
    if (isLiveActivityMetadata(liveActivity) && liveActivity.relays) {
      return liveActivity.relays;
    }
  }

  // NIP-29 groups and fallback: Use single relay URL
  const relayUrl = conversation.metadata?.relayUrl;
  return relayUrl ? [relayUrl] : [];
}

/**
 * Get the chat command identifier for a conversation
 * Returns a string that can be passed to the `chat` command to open this conversation
 *
 * For NIP-29 groups: relay'group-id (without wss:// prefix)
 * For NIP-53 live activities: naddr1... encoding
 */
function getChatIdentifier(conversation: Conversation): string | null {
  if (conversation.protocol === "nip-29") {
    const groupId = conversation.metadata?.groupId;
    const relayUrl = conversation.metadata?.relayUrl;
    if (!groupId || !relayUrl) return null;

    // Strip wss:// or ws:// prefix for cleaner identifier
    const cleanRelay = relayUrl.replace(/^wss?:\/\//, "");
    return `${cleanRelay}'${groupId}`;
  }

  if (conversation.protocol === "nip-53") {
    const activityAddress = conversation.metadata?.activityAddress;
    if (!activityAddress) return null;

    // Get relay hints from live activity metadata
    const liveActivity = conversation.metadata?.liveActivity;
    const relays = liveActivity?.relays || [];

    return nip19.naddrEncode({
      kind: activityAddress.kind,
      pubkey: activityAddress.pubkey,
      identifier: activityAddress.identifier,
      relays: relays.slice(0, 3), // Limit relay hints to keep naddr short
    });
  }

  return null;
}

/**
 * Conversation resolution result - either success with conversation or error
 */
type ConversationResult =
  | { status: "loading" }
  | { status: "success"; conversation: Conversation }
  | { status: "error"; error: string };

/**
 * ComposerReplyPreview - Shows who is being replied to in the composer
 */
const ComposerReplyPreview = memo(function ComposerReplyPreview({
  replyToId,
  onClear,
}: {
  replyToId: string;
  onClear: () => void;
}) {
  const replyEvent = use$(() => eventStore.event(replyToId), [replyToId]);

  if (!replyEvent) {
    return (
      <div className="flex items-center gap-2 rounded bg-muted px-2 py-1 text-xs mb-1.5 overflow-hidden">
        <span className="flex-1 min-w-0 truncate">
          Replying to {replyToId.slice(0, 8)}...
        </span>
        <button
          onClick={onClear}
          className="ml-auto text-muted-foreground hover:text-foreground flex-shrink-0"
        >
          ✕
        </button>
      </div>
    );
  }

  return (
    <div className="flex items-center gap-2 rounded bg-muted px-2 py-1 text-xs mb-1.5 overflow-hidden">
      <span className="flex-shrink-0">↳</span>
      <UserName
        pubkey={replyEvent.pubkey}
        className="font-medium flex-shrink-0"
      />
      <div className="flex-1 min-w-0 line-clamp-1 overflow-hidden text-muted-foreground">
        <RichText
          event={replyEvent}
          options={{ showMedia: false, showEventEmbeds: false }}
        />
      </div>
      <button
        onClick={onClear}
        className="ml-auto text-muted-foreground hover:text-foreground flex-shrink-0"
      >
        ✕
      </button>
    </div>
  );
});

/**
 * MessageItem - Memoized message component for performance
 */
const MessageItem = memo(function MessageItem({
  message,
  adapter,
  conversation,
  onReply,
  canReply,
  onScrollToMessage,
}: {
  message: Message;
  adapter: ChatProtocolAdapter;
  conversation: Conversation;
  onReply?: (messageId: string) => void;
  canReply: boolean;
  onScrollToMessage?: (messageId: string) => void;
}) {
  // Get relays for this conversation (memoized to prevent unnecessary re-subscriptions)
  const relays = useMemo(
    () => getConversationRelays(conversation),
    [conversation],
  );

  // System messages (join/leave) have special styling
  if (message.type === "system") {
    return (
      <div className="flex items-center px-3 py-1">
        <span className="text-xs text-muted-foreground">
          * <UserName pubkey={message.author} className="text-xs" />{" "}
          {message.content}
        </span>
      </div>
    );
  }

  // Zap messages have special styling with gradient border
  if (message.type === "zap") {
    const zapRequest = message.event ? getZapRequest(message.event) : null;
    // For NIP-57 zaps, reply target is in the zap request's e-tag
    // For NIP-61 nutzaps, reply target is already in message.replyTo
    const zapReplyTo =
      message.replyTo ||
      zapRequest?.tags.find((t) => t[0] === "e")?.[1] ||
      undefined;

    // Check if the replied-to event exists and is a chat kind
    const replyEvent = use$(
      () => (zapReplyTo ? eventStore.event(zapReplyTo) : undefined),
      [zapReplyTo],
    );

    // Only show reply preview if:
    // 1. The event exists in our store
    // 2. The event is a chat kind (includes messages, nutzaps, live chat, and zap receipts)
    const shouldShowReplyPreview =
      zapReplyTo &&
      replyEvent &&
      (CHAT_KINDS as readonly number[]).includes(replyEvent.kind);

    return (
      <div className="pl-2 my-1">
        <div
          className="p-[1px] rounded"
          style={{
            background:
              "linear-gradient(to right, rgb(250 204 21), rgb(251 146 60), rgb(168 85 247), rgb(34 211 238))",
          }}
        >
          <div className="bg-background px-1 rounded-sm">
            <div className="flex items-center gap-2">
              <UserName
                pubkey={message.author}
                className="font-semibold text-sm"
              />
              <Zap className="size-4 fill-yellow-500 text-yellow-500" />
              <span className="text-yellow-500 font-bold">
                {(message.metadata?.zapAmount || 0).toLocaleString("en", {
                  notation: "compact",
                })}
              </span>
              {message.metadata?.zapRecipient && (
                <UserName
                  pubkey={message.metadata.zapRecipient}
                  className="text-sm"
                />
              )}
              <span className="text-xs text-muted-foreground">
                <Timestamp timestamp={message.timestamp} />
              </span>
              {/* Reactions display - inline after timestamp */}
              <MessageReactions messageId={message.id} relays={relays} />
            </div>
            {shouldShowReplyPreview && (
              <ReplyPreview
                replyToId={zapReplyTo}
                adapter={adapter}
                conversation={conversation}
                onScrollToMessage={onScrollToMessage}
              />
            )}
            {message.content && (
              <RichText
                event={zapRequest || message.event}
                className="text-sm leading-tight break-words"
                options={{ showMedia: false, showEventEmbeds: false }}
              />
            )}
          </div>
        </div>
      </div>
    );
  }

  // Regular user messages - wrap in context menu if event exists
  const messageContent = (
    <div className="group flex items-start hover:bg-muted/50 px-3">
      <div className="flex-1 min-w-0">
        <div className="flex items-center gap-2">
          <UserName pubkey={message.author} className="font-semibold text-sm" />
          <span className="text-xs text-muted-foreground">
            <Timestamp timestamp={message.timestamp} />
          </span>
          {/* Reactions display - inline after timestamp */}
          <MessageReactions messageId={message.id} relays={relays} />
          {canReply && onReply && (
            <button
              onClick={() => onReply(message.id)}
              className="opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-foreground ml-auto"
              title="Reply to this message"
            >
              <Reply className="size-3" />
            </button>
          )}
        </div>
        <div className="break-words overflow-hidden">
          {message.event ? (
            <RichText className="text-sm leading-tight" event={message.event}>
              {message.replyTo && (
                <ReplyPreview
                  replyToId={message.replyTo}
                  adapter={adapter}
                  conversation={conversation}
                  onScrollToMessage={onScrollToMessage}
                />
              )}
            </RichText>
          ) : (
            <span className="whitespace-pre-wrap break-words">
              {message.content}
            </span>
          )}
        </div>
      </div>
    </div>
  );

  // Wrap in context menu if event exists
  if (message.event) {
    return (
      <ChatMessageContextMenu
        event={message.event}
        onReply={canReply && onReply ? () => onReply(message.id) : undefined}
        conversation={conversation}
        adapter={adapter}
      >
        {messageContent}
      </ChatMessageContextMenu>
    );
  }

  return messageContent;
});

/**
 * ChatViewer - Main chat interface component
 *
 * Provides protocol-agnostic chat UI that works across all Nostr messaging protocols.
 * Uses adapter pattern to handle protocol-specific logic while providing consistent UX.
 */
export function ChatViewer({
  protocol,
  identifier,
  customTitle,
  headerPrefix,
}: ChatViewerProps) {
  const { addWindow } = useGrimoire();

  // Get active account with signing capability
  const { pubkey, canSign, signer } = useAccount();

  // Profile search for mentions
  const { searchProfiles } = useProfileSearch();

  // Emoji search for custom emoji autocomplete
  const { searchEmojis } = useEmojiSearch();

  // Copy chat identifier to clipboard
  const { copy: copyChatId, copied: chatIdCopied } = useCopy();

  // Ref to MentionEditor for programmatic submission
  const editorRef = useRef<MentionEditorHandle>(null);

  // Blossom upload hook for file attachments
  const { open: openUpload, dialog: uploadDialog } = useBlossomUpload({
    accept: "image/*,video/*,audio/*",
    onSuccess: (results) => {
      if (results.length > 0 && editorRef.current) {
        // Insert the first successful upload as a blob attachment with metadata
        const { blob, server } = results[0];
        editorRef.current.insertBlob({
          url: blob.url,
          sha256: blob.sha256,
          mimeType: blob.type,
          size: blob.size,
          server,
        });
        editorRef.current.focus();
      }
    },
  });

  // Get the appropriate adapter for this protocol
  const adapter = useMemo(() => getAdapter(protocol), [protocol]);

  // State for retry trigger
  const [retryCount, setRetryCount] = useState(0);

  // Resolve conversation from identifier with error handling
  const conversationResult = use$(
    () =>
      from(adapter.resolveConversation(identifier)).pipe(
        map(
          (conv): ConversationResult => ({
            status: "success",
            conversation: conv,
          }),
        ),
        catchError((err) => {
          console.error("[Chat] Failed to resolve conversation:", err);
          const errorMessage =
            err instanceof Error ? err.message : "Failed to load conversation";
          return of<ConversationResult>({
            status: "error",
            error: errorMessage,
          });
        }),
      ),
    [adapter, identifier, retryCount],
  );

  // Extract conversation from result (null while loading or on error)
  const conversation =
    conversationResult?.status === "success"
      ? conversationResult.conversation
      : null;

  // Slash command search for action autocomplete
  // Context-aware: only shows relevant actions based on membership status
  const searchCommands = useCallback(
    async (query: string) => {
      const availableActions = adapter.getActions({
        conversation: conversation || undefined,
        activePubkey: pubkey,
      });
      const lowerQuery = query.toLowerCase();
      return availableActions.filter((action) =>
        action.name.toLowerCase().includes(lowerQuery),
      );
    },
    [adapter, conversation, pubkey],
  );

  // Cleanup subscriptions when conversation changes or component unmounts
  useEffect(() => {
    return () => {
      if (conversation) {
        adapter.cleanup(conversation.id);
      }
    };
  }, [adapter, conversation]);

  // Load messages for this conversation (reactive)
  const messages = use$(
    () => (conversation ? adapter.loadMessages(conversation) : undefined),
    [adapter, conversation],
  );

  // Process messages to include day markers
  const messagesWithMarkers = useMemo(() => {
    if (!messages || messages.length === 0) return [];

    const items: Array<
      | { type: "message"; data: Message }
      | { type: "day-marker"; data: string; timestamp: number }
    > = [];

    messages.forEach((message, index) => {
      // Add day marker if this is the first message or if day changed
      if (index === 0) {
        items.push({
          type: "day-marker",
          data: formatDayMarker(message.timestamp),
          timestamp: message.timestamp,
        });
      } else {
        const prevMessage = messages[index - 1];
        if (isDifferentDay(prevMessage.timestamp, message.timestamp)) {
          items.push({
            type: "day-marker",
            data: formatDayMarker(message.timestamp),
            timestamp: message.timestamp,
          });
        }
      }

      // Add the message itself
      items.push({ type: "message", data: message });
    });

    return items;
  }, [messages]);

  // Track reply context (which message is being replied to)
  const [replyTo, setReplyTo] = useState<string | undefined>();

  // State for loading older messages
  const [isLoadingOlder, setIsLoadingOlder] = useState(false);
  const [hasMore, setHasMore] = useState(true);

  // Ref to Virtuoso for programmatic scrolling
  const virtuosoRef = useRef<VirtuosoHandle>(null);

  // State for send in progress (prevents double-sends)
  const [isSending, setIsSending] = useState(false);

  // State for tooltip open (for mobile tap support)
  const [tooltipOpen, setTooltipOpen] = useState(false);

  // Handle sending messages with error handling
  const handleSend = async (
    content: string,
    replyToId?: string,
    emojiTags?: EmojiTag[],
    blobAttachments?: BlobAttachment[],
  ) => {
    if (!conversation || !canSign || isSending) return;

    // Check if this is a slash command
    const slashCmd = parseSlashCommand(content);
    if (slashCmd) {
      // Execute action instead of sending message
      setIsSending(true);
      try {
        const result = await adapter.executeAction(slashCmd.command, {
          activePubkey: pubkey!,
          activeSigner: signer!,
          conversation,
        });

        if (result.success) {
          toast.success(result.message || "Action completed");
        } else {
          toast.error(result.message || "Action failed");
        }
      } catch (error) {
        console.error("[Chat] Failed to execute action:", error);
        const errorMessage =
          error instanceof Error ? error.message : "Action failed";
        toast.error(errorMessage);
      } finally {
        setIsSending(false);
      }
      return;
    }

    // Regular message sending
    setIsSending(true);
    try {
      await adapter.sendMessage(conversation, content, {
        replyTo: replyToId,
        emojiTags,
        blobAttachments,
      });
      setReplyTo(undefined); // Clear reply context only on success
    } catch (error) {
      console.error("[Chat] Failed to send message:", error);
      const errorMessage =
        error instanceof Error ? error.message : "Failed to send message";
      toast.error(errorMessage);
      // Don't clear replyTo so user can retry
    } finally {
      setIsSending(false);
    }
  };

  // Handle command execution from autocomplete
  const handleCommandExecute = useCallback(
    async (action: ChatAction) => {
      if (!conversation || !canSign || isSending) return;

      setIsSending(true);
      try {
        const result = await adapter.executeAction(action.name, {
          activePubkey: pubkey!,
          activeSigner: signer!,
          conversation,
        });

        if (result.success) {
          toast.success(result.message || "Action completed");
        } else {
          toast.error(result.message || "Action failed");
        }
      } catch (error) {
        console.error("[Chat] Failed to execute action:", error);
        const errorMessage =
          error instanceof Error ? error.message : "Action failed";
        toast.error(errorMessage);
      } finally {
        setIsSending(false);
      }
    },
    [conversation, canSign, isSending, adapter, pubkey, signer],
  );

  // Handle reply button click
  const handleReply = useCallback((messageId: string) => {
    setReplyTo(messageId);
    // Focus the editor so user can start typing immediately
    editorRef.current?.focus();
  }, []);

  // Handle scroll to message (when clicking on reply preview)
  // Must search in messagesWithMarkers since that's what Virtuoso renders
  const handleScrollToMessage = useCallback(
    (messageId: string) => {
      if (!messagesWithMarkers) return;
      // Find index in the rendered array (which includes day markers)
      const index = messagesWithMarkers.findIndex(
        (item) => item.type === "message" && item.data.id === messageId,
      );
      if (index !== -1 && virtuosoRef.current) {
        virtuosoRef.current.scrollToIndex({
          index,
          align: "center",
          behavior: "smooth",
        });
      }
    },
    [messagesWithMarkers],
  );

  // Handle loading older messages
  const handleLoadOlder = useCallback(async () => {
    if (!conversation || !messages || messages.length === 0 || isLoadingOlder) {
      return;
    }

    setIsLoadingOlder(true);
    try {
      // Get the timestamp of the oldest message
      const oldestMessage = messages[0];
      const olderMessages = await adapter.loadMoreMessages(
        conversation,
        oldestMessage.timestamp,
      );

      // If we got fewer messages than expected, there might be no more
      if (olderMessages.length < 50) {
        setHasMore(false);
      }
    } catch (error) {
      console.error("Failed to load older messages:", error);
    } finally {
      setIsLoadingOlder(false);
    }
  }, [conversation, messages, adapter, isLoadingOlder]);

  // Handle NIP badge click
  const handleNipClick = useCallback(() => {
    if (conversation?.protocol === "nip-29") {
      addWindow("nip", { number: 29 });
    } else if (conversation?.protocol === "nip-53") {
      addWindow("nip", { number: 53 });
    }
  }, [conversation?.protocol, addWindow]);

  // Get live activity metadata if this is a NIP-53 chat (with type guard)
  const liveActivity = isLiveActivityMetadata(
    conversation?.metadata?.liveActivity,
  )
    ? conversation?.metadata?.liveActivity
    : undefined;

  // Derive participants from messages for live activities (unique pubkeys who have chatted)
  const derivedParticipants = useMemo(() => {
    if (conversation?.type !== "live-chat" || !messages) {
      return conversation?.participants || [];
    }

    const hostPubkey = liveActivity?.hostPubkey;
    const participants: { pubkey: string; role: "host" | "member" }[] = [];

    // Host always first
    if (hostPubkey) {
      participants.push({ pubkey: hostPubkey, role: "host" });
    }

    // Add other participants from messages (excluding host)
    const seen = new Set(hostPubkey ? [hostPubkey] : []);
    for (const msg of messages) {
      if (msg.type !== "system" && !seen.has(msg.author)) {
        seen.add(msg.author);
        participants.push({ pubkey: msg.author, role: "member" });
      }
    }

    return participants;
  }, [
    conversation?.type,
    conversation?.participants,
    messages,
    liveActivity?.hostPubkey,
  ]);

  // Handle loading state
  if (!conversationResult || conversationResult.status === "loading") {
    return (
      <div className="flex h-full flex-col items-center justify-center gap-2 text-muted-foreground">
        <Loader2 className="size-6 animate-spin" />
        <span className="text-xs">Loading conversation...</span>
      </div>
    );
  }

  // Handle error state with retry option
  if (conversationResult.status === "error") {
    return (
      <div className="flex h-full flex-col items-center justify-center gap-3 text-muted-foreground p-4">
        <AlertTriangle className="size-8 text-destructive" />
        <span className="text-center text-sm">{conversationResult.error}</span>
        <Button
          variant="outline"
          size="sm"
          onClick={() => setRetryCount((c) => c + 1)}
          className="gap-2"
        >
          <RefreshCw className="size-3" />
          Retry
        </Button>
      </div>
    );
  }

  // At this point conversation is guaranteed to exist
  if (!conversation) {
    return null; // Should never happen, but satisfies TypeScript
  }

  return (
    <div className="flex h-full flex-col">
      {/* Header with conversation info and controls */}
      <div className="pl-2 pr-0 border-b w-full py-0.5">
        <div className="flex items-center justify-between gap-3">
          <div className="flex flex-1 min-w-0 items-center gap-2">
            {headerPrefix}
            <TooltipProvider>
              <Tooltip open={tooltipOpen} onOpenChange={setTooltipOpen}>
                <TooltipTrigger asChild>
                  <button
                    className="text-sm font-semibold truncate cursor-help text-left"
                    onClick={() => setTooltipOpen(!tooltipOpen)}
                  >
                    {customTitle || conversation.title}
                  </button>
                </TooltipTrigger>
                <TooltipContent
                  side="bottom"
                  align="start"
                  className="max-w-md p-3"
                >
                  <div className="flex flex-col gap-2">
                    {/* Icon + Name */}
                    <div className="flex items-center gap-2">
                      {conversation.metadata?.icon && (
                        <img
                          src={conversation.metadata.icon}
                          alt=""
                          className="size-6 rounded object-cover flex-shrink-0"
                          onError={(e) => {
                            // Hide image if it fails to load
                            e.currentTarget.style.display = "none";
                          }}
                        />
                      )}
                      <span className="font-semibold">
                        {conversation.title}
                      </span>
                    </div>
                    {/* Description */}
                    {conversation.metadata?.description && (
                      <p className="text-xs text-primary-foreground/90">
                        {conversation.metadata.description}
                      </p>
                    )}
                    {/* Protocol Type - Clickable */}
                    <div className="flex items-center gap-1.5 text-xs">
                      {(conversation.type === "group" ||
                        conversation.type === "live-chat") && (
                        <button
                          onClick={(e) => {
                            e.stopPropagation();
                            handleNipClick();
                          }}
                          className="rounded bg-primary-foreground/20 px-1.5 py-0.5 font-mono hover:bg-primary-foreground/30 transition-colors cursor-pointer text-primary-foreground"
                        >
                          {conversation.protocol.toUpperCase()}
                        </button>
                      )}
                      {(conversation.type === "group" ||
                        conversation.type === "live-chat") && (
                        <span className="text-primary-foreground/60">•</span>
                      )}
                      <span className="capitalize text-primary-foreground/80">
                        {conversation.type}
                      </span>
                    </div>
                    {/* Live Activity Status */}
                    {liveActivity?.status && (
                      <div className="flex items-center gap-1.5 text-xs">
                        <span className="text-primary-foreground/80">
                          Status:
                        </span>
                        <StatusBadge status={liveActivity.status} size="xs" />
                      </div>
                    )}
                    {/* Host Info */}
                    {liveActivity?.hostPubkey && (
                      <div className="flex items-center gap-1.5 text-xs text-primary-foreground/80">
                        <span>Host:</span>
                        <UserName
                          pubkey={liveActivity.hostPubkey}
                          className="text-xs text-primary-foreground"
                        />
                      </div>
                    )}
                  </div>
                </TooltipContent>
              </Tooltip>
            </TooltipProvider>
            {/* Copy Chat ID button */}
            {getChatIdentifier(conversation) && (
              <button
                onClick={() => {
                  const chatId = getChatIdentifier(conversation);
                  if (chatId) copyChatId(chatId);
                }}
                className="text-muted-foreground hover:text-foreground transition-colors flex-shrink-0"
                aria-label="Copy chat ID"
              >
                {chatIdCopied ? (
                  <CopyCheck className="size-3.5" />
                ) : (
                  <Copy className="size-3.5" />
                )}
              </button>
            )}
          </div>
          <div className="flex items-center gap-2 text-xs text-muted-foreground p-1">
            <MembersDropdown participants={derivedParticipants} />
            <RelaysDropdown conversation={conversation} />
            {(conversation.type === "group" ||
              conversation.type === "live-chat") && (
              <button
                onClick={handleNipClick}
                className="rounded bg-muted px-1.5 py-0.5 font-mono hover:bg-muted/80 transition-colors cursor-pointer"
              >
                {conversation.protocol.toUpperCase()}
              </button>
            )}
          </div>
        </div>
      </div>

      {/* Message timeline with virtualization */}
      <div className="flex-1 overflow-hidden">
        {messagesWithMarkers && messagesWithMarkers.length > 0 ? (
          <Virtuoso
            ref={virtuosoRef}
            data={messagesWithMarkers}
            initialTopMostItemIndex={messagesWithMarkers.length - 1}
            followOutput="smooth"
            alignToBottom
            components={{
              Header: () =>
                hasMore && conversationResult.status === "success" ? (
                  <div className="flex justify-center py-2">
                    <Button
                      onClick={handleLoadOlder}
                      disabled={isLoadingOlder}
                      variant="ghost"
                      size="sm"
                    >
                      {isLoadingOlder ? (
                        <>
                          <Loader2 className="size-3 animate-spin" />
                          <span className="text-xs">Loading...</span>
                        </>
                      ) : (
                        "Load older messages"
                      )}
                    </Button>
                  </div>
                ) : null,
              Footer: () => <div className="h-1" />,
            }}
            itemContent={(_index, item) => {
              if (item.type === "day-marker") {
                return (
                  <div
                    className="flex justify-center py-2"
                    key={`marker-${item.timestamp}`}
                  >
                    <Label className="text-[10px] text-muted-foreground">
                      {item.data}
                    </Label>
                  </div>
                );
              }
              return (
                <MessageItem
                  key={item.data.id}
                  message={item.data}
                  adapter={adapter}
                  conversation={conversation}
                  onReply={handleReply}
                  canReply={canSign}
                  onScrollToMessage={handleScrollToMessage}
                />
              );
            }}
            style={{ height: "100%" }}
          />
        ) : (
          <div className="flex h-full items-center justify-center text-sm text-muted-foreground">
            No messages yet. Start the conversation!
          </div>
        )}
      </div>

      {/* Message composer - only show if user can sign */}
      {canSign ? (
        <div className="border-t px-2 py-1 pb-0">
          {replyTo && (
            <ComposerReplyPreview
              replyToId={replyTo}
              onClear={() => setReplyTo(undefined)}
            />
          )}
          <div className="flex gap-1.5 items-center">
            <TooltipProvider>
              <Tooltip>
                <TooltipTrigger asChild>
                  <Button
                    type="button"
                    variant="ghost"
                    size="icon"
                    className="flex-shrink-0 size-7 text-muted-foreground hover:text-foreground"
                    onClick={openUpload}
                    disabled={isSending}
                  >
                    <Paperclip className="size-4" />
                  </Button>
                </TooltipTrigger>
                <TooltipContent side="top">
                  <p>Attach media</p>
                </TooltipContent>
              </Tooltip>
            </TooltipProvider>
            <MentionEditor
              ref={editorRef}
              placeholder="Type a message..."
              searchProfiles={searchProfiles}
              searchEmojis={searchEmojis}
              searchCommands={searchCommands}
              onCommandExecute={handleCommandExecute}
              onSubmit={(content, emojiTags, blobAttachments) => {
                if (content.trim()) {
                  handleSend(content, replyTo, emojiTags, blobAttachments);
                }
              }}
              className="flex-1 min-w-0"
            />
            <Button
              type="button"
              variant="secondary"
              size="sm"
              className="flex-shrink-0 h-7 px-2 text-xs"
              disabled={isSending}
              onClick={() => {
                editorRef.current?.submit();
              }}
            >
              {isSending ? <Loader2 className="size-3 animate-spin" /> : "Send"}
            </Button>
          </div>
          {uploadDialog}
        </div>
      ) : (
        <div className="border-t px-3 py-2 text-center text-sm text-muted-foreground">
          Sign in to send messages
        </div>
      )}
    </div>
  );
}

/**
 * Get the appropriate adapter for a protocol
 * Currently NIP-29 (relay-based groups) and NIP-53 (live activity chat) are supported
 * Other protocols will be enabled in future phases
 */
function getAdapter(protocol: ChatProtocol): ChatProtocolAdapter {
  switch (protocol) {
    // case "nip-c7":  // Phase 1 - Simple chat (coming soon)
    //   return new NipC7Adapter();
    case "nip-29":
      return new Nip29Adapter();
    // case "nip-17":  // Phase 2 - Encrypted DMs (coming soon)
    //   return new Nip17Adapter();
    // case "nip-28":  // Phase 3 - Public channels (coming soon)
    //   return new Nip28Adapter();
    case "nip-53":
      return new Nip53Adapter();
    default:
      throw new Error(`Unsupported protocol: ${protocol}`);
  }
}
