import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { Check, ChevronDown, Globe2, LogOut, MessageSquare, Plus, Search, SendHorizontal, Trash2 } from "lucide-preact";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Separator } from "@/components/ui/separator";
import { AuthScreen } from "@/components/auth/auth-screen";
import { ChatMessagesPanel } from "@/components/chat/chat-messages-panel";
import { SearchResultsPanel } from "@/components/search/search-results-panel";
import {
  createChat,
  createSearch,
  deleteChat,
  deleteSearch,
  getChat,
  listModels,
  getSearch,
  listChats,
  listSearches,
  runCompletion,
  runSearchStream,
  type ModelCatalogResponse,
  type Provider,
  type ChatDetail,
  type ChatSummary,
  type CompletionRequestMessage,
  type Message,
  type SearchDetail,
  type SearchSummary,
} from "@/lib/api";
import { useSessionAuth } from "@/hooks/use-session-auth";
import { cn } from "@/lib/utils";

type SidebarSelection = { kind: "chat" | "search"; id: string };
type DraftSelectionKind = "chat" | "search";
type SidebarItem = SidebarSelection & {
  title: string;
  updatedAt: string;
  createdAt: string;
};
type ContextMenuState = {
  item: SidebarSelection;
  x: number;
  y: number;
};

const PROVIDER_FALLBACK_MODELS: Record<Provider, string[]> = {
  openai: ["gpt-4.1-mini"],
  anthropic: ["claude-3-5-sonnet-latest"],
  xai: ["grok-3-mini"],
};

const EMPTY_MODEL_CATALOG: ModelCatalogResponse["providers"] = {
  openai: { models: [], loadedAt: null, error: null },
  anthropic: { models: [], loadedAt: null, error: null },
  xai: { models: [], loadedAt: null, error: null },
};

const MODEL_PREFERENCES_STORAGE_KEY = "sybil:modelPreferencesByProvider";

type ProviderModelPreferences = Record<Provider, string | null>;

const EMPTY_MODEL_PREFERENCES: ProviderModelPreferences = {
  openai: null,
  anthropic: null,
  xai: null,
};

function getModelOptions(catalog: ModelCatalogResponse["providers"], provider: Provider) {
  const providerModels = catalog[provider]?.models ?? [];
  if (providerModels.length) return providerModels;
  return PROVIDER_FALLBACK_MODELS[provider];
}

function loadStoredModelPreferences() {
  if (typeof window === "undefined") return EMPTY_MODEL_PREFERENCES;
  try {
    const raw = window.localStorage.getItem(MODEL_PREFERENCES_STORAGE_KEY);
    if (!raw) return EMPTY_MODEL_PREFERENCES;
    const parsed = JSON.parse(raw) as Partial<Record<Provider, string>>;
    return {
      openai: typeof parsed.openai === "string" && parsed.openai.trim() ? parsed.openai.trim() : null,
      anthropic: typeof parsed.anthropic === "string" && parsed.anthropic.trim() ? parsed.anthropic.trim() : null,
      xai: typeof parsed.xai === "string" && parsed.xai.trim() ? parsed.xai.trim() : null,
    };
  } catch {
    return EMPTY_MODEL_PREFERENCES;
  }
}

function pickProviderModel(options: string[], preferred: string | null, fallback: string | null = null) {
  if (preferred && options.includes(preferred)) return preferred;
  if (fallback && options.includes(fallback)) return fallback;
  return options[0] ?? "";
}

type ModelComboboxProps = {
  options: string[];
  value: string;
  onChange: (value: string) => void;
  disabled?: boolean;
};

function ModelCombobox({ options, value, onChange, disabled = false }: ModelComboboxProps) {
  const [open, setOpen] = useState(false);
  const [query, setQuery] = useState("");
  const rootRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);

  const filteredOptions = useMemo(() => {
    const needle = query.trim().toLowerCase();
    if (!needle) return options;
    return options.filter((option) => option.toLowerCase().includes(needle));
  }, [options, query]);

  useEffect(() => {
    if (!open) return;
    inputRef.current?.focus();
  }, [open]);

  useEffect(() => {
    if (!open) return;

    const handlePointerDown = (event: PointerEvent) => {
      if (rootRef.current?.contains(event.target as Node)) return;
      setOpen(false);
      setQuery("");
    };

    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key !== "Escape") return;
      setOpen(false);
      setQuery("");
    };

    window.addEventListener("pointerdown", handlePointerDown);
    window.addEventListener("keydown", handleKeyDown);
    return () => {
      window.removeEventListener("pointerdown", handlePointerDown);
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, [open]);

  return (
    <div className="relative" ref={rootRef}>
      <button
        type="button"
        className="flex h-9 min-w-56 items-center justify-between rounded-md border border-input bg-background px-2 text-sm"
        onClick={() => {
          if (disabled) return;
          setOpen((current) => !current);
        }}
        disabled={disabled}
      >
        <span className="truncate text-left">{value || "Select model"}</span>
        <ChevronDown className="ml-2 h-4 w-4 shrink-0 text-muted-foreground" />
      </button>
      {open ? (
        <div className="absolute right-0 z-50 mt-1 w-full rounded-md border border-border bg-background p-1 shadow-md">
          <input
            ref={inputRef}
            value={query}
            onInput={(event) => setQuery(event.currentTarget.value)}
            className="mb-1 h-8 w-full rounded-sm border border-input bg-background px-2 text-sm outline-none"
            placeholder="Filter models"
          />
          <div className="max-h-64 overflow-y-auto">
            {filteredOptions.length ? (
              filteredOptions.map((option) => (
                <button
                  key={option}
                  type="button"
                  className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm hover:bg-muted"
                  onClick={() => {
                    onChange(option);
                    setOpen(false);
                    setQuery("");
                  }}
                >
                  <Check className={cn("h-4 w-4", option === value ? "opacity-100" : "opacity-0")} />
                  <span className="truncate">{option}</span>
                </button>
              ))
            ) : (
              <p className="px-2 py-2 text-sm text-muted-foreground">No models found</p>
            )}
          </div>
        </div>
      ) : null}
    </div>
  );
}

function getChatTitle(chat: Pick<ChatSummary, "title">, messages?: ChatDetail["messages"]) {
  if (chat.title?.trim()) return chat.title.trim();
  const firstUserMessage = messages?.find((m) => m.role === "user")?.content.trim();
  if (firstUserMessage) return firstUserMessage.slice(0, 48);
  return "New chat";
}

function getSearchTitle(search: Pick<SearchSummary, "title" | "query">) {
  if (search.title?.trim()) return search.title.trim();
  if (search.query?.trim()) return search.query.trim().slice(0, 64);
  return "New search";
}

function buildSidebarItems(chats: ChatSummary[], searches: SearchSummary[]): SidebarItem[] {
  const items: SidebarItem[] = [
    ...chats.map((chat) => ({
      kind: "chat" as const,
      id: chat.id,
      title: getChatTitle(chat),
      updatedAt: chat.updatedAt,
      createdAt: chat.createdAt,
    })),
    ...searches.map((search) => ({
      kind: "search" as const,
      id: search.id,
      title: getSearchTitle(search),
      updatedAt: search.updatedAt,
      createdAt: search.createdAt,
    })),
  ];

  return items.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
}

function formatDate(value: string) {
  return new Intl.DateTimeFormat(undefined, {
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "2-digit",
  }).format(new Date(value));
}

export default function App() {
  const {
    authTokenInput,
    setAuthTokenInput,
    isCheckingSession,
    isSigningIn,
    isAuthenticated,
    authMode,
    authError,
    handleAuthFailure: baseHandleAuthFailure,
    handleSignIn,
    logout,
  } = useSessionAuth();

  const [chats, setChats] = useState<ChatSummary[]>([]);
  const [searches, setSearches] = useState<SearchSummary[]>([]);
  const [selectedItem, setSelectedItem] = useState<SidebarSelection | null>(null);
  const [selectedChat, setSelectedChat] = useState<ChatDetail | null>(null);
  const [selectedSearch, setSelectedSearch] = useState<SearchDetail | null>(null);
  const [draftKind, setDraftKind] = useState<DraftSelectionKind | null>(null);
  const [isLoadingCollections, setIsLoadingCollections] = useState(false);
  const [isLoadingSelection, setIsLoadingSelection] = useState(false);
  const [isSending, setIsSending] = useState(false);
  const [pendingChatState, setPendingChatState] = useState<{ chatId: string | null; messages: Message[] } | null>(null);
  const [composer, setComposer] = useState("");
  const [provider, setProvider] = useState<Provider>("openai");
  const [modelCatalog, setModelCatalog] = useState<ModelCatalogResponse["providers"]>(EMPTY_MODEL_CATALOG);
  const [providerModelPreferences, setProviderModelPreferences] = useState<ProviderModelPreferences>(() => loadStoredModelPreferences());
  const [model, setModel] = useState(() => {
    const stored = loadStoredModelPreferences();
    return stored.openai ?? PROVIDER_FALLBACK_MODELS.openai[0];
  });
  const [error, setError] = useState<string | null>(null);
  const transcriptEndRef = useRef<HTMLDivElement>(null);
  const contextMenuRef = useRef<HTMLDivElement>(null);
  const searchRunAbortRef = useRef<AbortController | null>(null);
  const searchRunCounterRef = useRef(0);
  const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);

  useEffect(() => {
    if (typeof document === "undefined") return;
    const textarea = document.getElementById("composer-input") as HTMLTextAreaElement | null;
    if (!textarea) return;
    textarea.style.height = "0px";
    textarea.style.height = `${textarea.scrollHeight}px`;
  }, [composer]);

  const sidebarItems = useMemo(() => buildSidebarItems(chats, searches), [chats, searches]);

  const resetWorkspaceState = () => {
    setChats([]);
    setSearches([]);
    setSelectedItem(null);
    setSelectedChat(null);
    setSelectedSearch(null);
    setDraftKind(null);
    setPendingChatState(null);
    setComposer("");
    setError(null);
  };

  const handleAuthFailure = (message: string) => {
    baseHandleAuthFailure(message);
    resetWorkspaceState();
  };

  const refreshCollections = async (preferredSelection?: SidebarSelection) => {
    setIsLoadingCollections(true);
    try {
      const [nextChats, nextSearches] = await Promise.all([listChats(), listSearches()]);
      const nextItems = buildSidebarItems(nextChats, nextSearches);
      setChats(nextChats);
      setSearches(nextSearches);

      setSelectedItem((current) => {
        const hasItem = (candidate: SidebarSelection | null) => {
          if (!candidate) return false;
          return nextItems.some((item) => item.kind === candidate.kind && item.id === candidate.id);
        };

        if (preferredSelection && hasItem(preferredSelection)) {
          return preferredSelection;
        }
        if (hasItem(current)) {
          return current;
        }
        const first = nextItems[0];
        return first ? { kind: first.kind, id: first.id } : null;
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    } finally {
      setIsLoadingCollections(false);
    }
  };

  const refreshModels = async () => {
    try {
      const data = await listModels();
      setModelCatalog(data.providers);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    }
  };

  const refreshChat = async (chatId: string) => {
    setIsLoadingSelection(true);
    try {
      const chat = await getChat(chatId);
      setSelectedChat(chat);
      setSelectedSearch(null);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    } finally {
      setIsLoadingSelection(false);
    }
  };

  const refreshSearch = async (searchId: string) => {
    setIsLoadingSelection(true);
    try {
      const search = await getSearch(searchId);
      setSelectedSearch(search);
      setSelectedChat(null);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    } finally {
      setIsLoadingSelection(false);
    }
  };

  useEffect(() => {
    if (!isAuthenticated) return;
    void Promise.all([refreshCollections(), refreshModels()]);
  }, [isAuthenticated]);

  const providerModelOptions = useMemo(() => getModelOptions(modelCatalog, provider), [modelCatalog, provider]);

  useEffect(() => {
    setModel((current) => {
      return pickProviderModel(providerModelOptions, providerModelPreferences[provider], current);
    });
  }, [provider, providerModelOptions, providerModelPreferences]);

  useEffect(() => {
    if (typeof window === "undefined") return;
    window.localStorage.setItem(MODEL_PREFERENCES_STORAGE_KEY, JSON.stringify(providerModelPreferences));
  }, [providerModelPreferences]);

  const selectedKey = selectedItem ? `${selectedItem.kind}:${selectedItem.id}` : null;

  useEffect(() => {
    if (!isAuthenticated) {
      setSelectedChat(null);
      setSelectedSearch(null);
      return;
    }
    if (!selectedItem) {
      setSelectedChat(null);
      setSelectedSearch(null);
      return;
    }

    if (selectedItem.kind === "chat") {
      void refreshChat(selectedItem.id);
      return;
    }
    void refreshSearch(selectedItem.id);
  }, [isAuthenticated, selectedKey]);

  useEffect(() => {
    if (draftKind === "search" || selectedItem?.kind === "search") return;
    transcriptEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
  }, [draftKind, selectedChat?.messages.length, isSending, selectedItem?.kind]);

  useEffect(() => {
    return () => {
      searchRunAbortRef.current?.abort();
      searchRunAbortRef.current = null;
    };
  }, []);

  const messages = selectedChat?.messages ?? [];
  const isSearchMode = draftKind ? draftKind === "search" : selectedItem?.kind === "search";
  const isSearchRunning = isSending && isSearchMode;
  const displayMessages = useMemo(() => {
    if (!pendingChatState) return messages;
    if (pendingChatState.chatId) {
      if (selectedItem?.kind === "chat" && selectedItem.id === pendingChatState.chatId) {
        return pendingChatState.messages;
      }
      return messages;
    }
    return isSearchMode ? messages : pendingChatState.messages;
  }, [isSearchMode, messages, pendingChatState, selectedItem]);

  const selectedChatSummary = useMemo(() => {
    if (!selectedItem || selectedItem.kind !== "chat") return null;
    return chats.find((chat) => chat.id === selectedItem.id) ?? null;
  }, [chats, selectedItem]);

  const selectedSearchSummary = useMemo(() => {
    if (!selectedItem || selectedItem.kind !== "search") return null;
    return searches.find((search) => search.id === selectedItem.id) ?? null;
  }, [searches, selectedItem]);

  const selectedTitle = useMemo(() => {
    if (draftKind === "chat") return "New chat";
    if (draftKind === "search") return "New search";
    if (!selectedItem) return "Sybil";
    if (selectedItem.kind === "chat") {
      if (selectedChat) return getChatTitle(selectedChat, selectedChat.messages);
      if (selectedChatSummary) return getChatTitle(selectedChatSummary);
      return "New chat";
    }
    if (selectedSearch) return getSearchTitle(selectedSearch);
    if (selectedSearchSummary) return getSearchTitle(selectedSearchSummary);
    return "New search";
  }, [draftKind, selectedChat, selectedChatSummary, selectedItem, selectedSearch, selectedSearchSummary]);

  const handleCreateChat = () => {
    setError(null);
    setContextMenu(null);
    setDraftKind("chat");
    setSelectedItem(null);
    setSelectedChat(null);
    setSelectedSearch(null);
  };

  const handleCreateSearch = () => {
    setError(null);
    setContextMenu(null);
    setDraftKind("search");
    setSelectedItem(null);
    setSelectedChat(null);
    setSelectedSearch(null);
  };

  const openContextMenu = (event: MouseEvent, item: SidebarSelection) => {
    event.preventDefault();
    const menuWidth = 160;
    const menuHeight = 40;
    const padding = 8;
    const x = Math.min(event.clientX, window.innerWidth - menuWidth - padding);
    const y = Math.min(event.clientY, window.innerHeight - menuHeight - padding);
    setContextMenu({ item, x: Math.max(padding, x), y: Math.max(padding, y) });
  };

  const handleDeleteFromContextMenu = async () => {
    if (!contextMenu || isSending) return;
    const target = contextMenu.item;
    setContextMenu(null);
    setError(null);
    try {
      if (target.kind === "chat") {
        await deleteChat(target.id);
      } else {
        await deleteSearch(target.id);
      }
      await refreshCollections();
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    }
  };

  useEffect(() => {
    if (!contextMenu) return;
    const handlePointerDown = (event: PointerEvent) => {
      if (contextMenuRef.current?.contains(event.target as Node)) return;
      setContextMenu(null);
    };
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") setContextMenu(null);
    };
    window.addEventListener("pointerdown", handlePointerDown);
    window.addEventListener("keydown", handleKeyDown);
    return () => {
      window.removeEventListener("pointerdown", handlePointerDown);
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, [contextMenu]);

  const handleSendChat = async (content: string) => {
    const optimisticUserMessage: Message = {
      id: `temp-user-${Date.now()}`,
      createdAt: new Date().toISOString(),
      role: "user",
      content,
      name: null,
    };

    const optimisticAssistantMessage: Message = {
      id: `temp-assistant-${Date.now()}`,
      createdAt: new Date().toISOString(),
      role: "assistant",
      content: "",
      name: null,
    };

    setPendingChatState({
      chatId: selectedItem?.kind === "chat" ? selectedItem.id : null,
      messages: (selectedChat?.messages ?? []).concat(optimisticUserMessage, optimisticAssistantMessage),
    });

    let chatId = draftKind === "chat" ? null : selectedItem?.kind === "chat" ? selectedItem.id : null;

    if (!chatId) {
      const chat = await createChat();
      chatId = chat.id;
      setDraftKind(null);
      setSelectedItem({ kind: "chat", id: chatId });
      setPendingChatState((current) => (current ? { ...current, chatId } : current));
      setSelectedChat({
        id: chat.id,
        title: chat.title,
        createdAt: chat.createdAt,
        updatedAt: chat.updatedAt,
        messages: [],
      });
      setSelectedSearch(null);
    }

    if (!chatId) {
      throw new Error("Unable to initialize chat");
    }

    let baseChat = selectedChat;
    if (!baseChat || baseChat.id !== chatId) {
      baseChat = await getChat(chatId);
    }

    const requestMessages: CompletionRequestMessage[] = [
      ...baseChat.messages.map((message) => ({
        role: message.role,
        content: message.content,
        ...(message.name ? { name: message.name } : {}),
      })),
      {
        role: "user",
        content,
      },
    ];

    const selectedModel = model.trim();
    if (!selectedModel) {
      throw new Error("No model available for selected provider");
    }

    await runCompletion({
      chatId,
      provider,
      model: selectedModel,
      messages: requestMessages,
    });

    await Promise.all([refreshCollections({ kind: "chat", id: chatId }), refreshChat(chatId)]);
    setPendingChatState(null);
  };

  const handleSendSearch = async (query: string) => {
    const runId = ++searchRunCounterRef.current;
    searchRunAbortRef.current?.abort();
    const abortController = new AbortController();
    searchRunAbortRef.current = abortController;

    let searchId = draftKind === "search" ? null : selectedItem?.kind === "search" ? selectedItem.id : null;

    if (!searchId) {
      const search = await createSearch({
        query,
        title: query.slice(0, 80),
      });
      searchId = search.id;
      setDraftKind(null);
      setSelectedItem({ kind: "search", id: searchId });
    }

    if (!searchId) {
      throw new Error("Unable to initialize search");
    }

    const nowIso = new Date().toISOString();
    setSelectedSearch((current) => {
      if (!current || current.id !== searchId) {
        return {
          id: searchId,
          title: query.slice(0, 80),
          query,
          createdAt: nowIso,
          updatedAt: nowIso,
          requestId: null,
          latencyMs: null,
          error: null,
          answerText: null,
          answerRequestId: null,
          answerCitations: null,
          answerError: null,
          results: [],
        };
      }
      return {
        ...current,
        title: query.slice(0, 80),
        query,
        error: null,
        latencyMs: null,
        answerText: null,
        answerRequestId: null,
        answerCitations: null,
        answerError: null,
        results: [],
      };
    });

    try {
      await runSearchStream(
        searchId,
        {
          query,
          title: query.slice(0, 80),
          type: "auto",
          numResults: 10,
        },
        {
          onSearchResults: (payload) => {
            if (runId !== searchRunCounterRef.current) return;
            setSelectedSearch((current) => {
              if (!current || current.id !== searchId) return current;
              return {
                ...current,
                requestId: payload.requestId ?? current.requestId,
                error: null,
                results: payload.results,
              };
            });
          },
          onSearchError: (payload) => {
            if (runId !== searchRunCounterRef.current) return;
            setSelectedSearch((current) => {
              if (!current || current.id !== searchId) return current;
              return { ...current, error: payload.error };
            });
          },
          onAnswer: (payload) => {
            if (runId !== searchRunCounterRef.current) return;
            setSelectedSearch((current) => {
              if (!current || current.id !== searchId) return current;
              return {
                ...current,
                answerText: payload.answerText,
                answerRequestId: payload.answerRequestId,
                answerCitations: payload.answerCitations,
                answerError: null,
              };
            });
          },
          onAnswerError: (payload) => {
            if (runId !== searchRunCounterRef.current) return;
            setSelectedSearch((current) => {
              if (!current || current.id !== searchId) return current;
              return { ...current, answerError: payload.error };
            });
          },
          onDone: (payload) => {
            if (runId !== searchRunCounterRef.current) return;
            setSelectedSearch(payload.search);
            setSelectedChat(null);
          },
          onError: (payload) => {
            if (runId !== searchRunCounterRef.current) return;
            setError(payload.message);
          },
        },
        { signal: abortController.signal }
      );
    } catch (err) {
      if (abortController.signal.aborted) return;
      throw err;
    } finally {
      if (runId === searchRunCounterRef.current) {
        searchRunAbortRef.current = null;
      }
    }

    await refreshCollections({ kind: "search", id: searchId });
  };

  const handleSend = async () => {
    const content = composer.trim();
    if (!content || isSending) return;

    setComposer("");
    setError(null);
    setIsSending(true);

    try {
      if (isSearchMode) {
        await handleSendSearch(content);
      } else {
        await handleSendChat(content);
      }
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }

      if (!isSearchMode) {
        setPendingChatState(null);
      }

      if (selectedItem?.kind === "chat") {
        await refreshChat(selectedItem.id);
      }
      if (selectedItem?.kind === "search") {
        await refreshSearch(selectedItem.id);
      }
    } finally {
      setIsSending(false);
    }
  };

  const handleLogout = () => {
    setContextMenu(null);
    logout();
    resetWorkspaceState();
  };

  if (isCheckingSession) {
    return (
      <div className="flex h-full items-center justify-center">
        <p className="text-sm text-muted-foreground">Checking session...</p>
      </div>
    );
  }

  if (!isAuthenticated) {
    return (
      <AuthScreen
        authTokenInput={authTokenInput}
        setAuthTokenInput={setAuthTokenInput}
        isSigningIn={isSigningIn}
        authError={authError}
        onSignIn={handleSignIn}
      />
    );
  }

  return (
    <div className="h-full">
      <div className="flex h-full w-full overflow-hidden bg-background">
        <aside className="flex w-80 shrink-0 flex-col border-r bg-[hsl(272_34%_14%)]">
          <div className="grid grid-cols-2 gap-2 p-3">
            <Button className="justify-start gap-2" onClick={handleCreateChat}>
              <Plus className="h-4 w-4" />
              New chat
            </Button>
            <Button className="justify-start gap-2" variant="secondary" onClick={handleCreateSearch}>
              <Search className="h-4 w-4" />
              New search
            </Button>
          </div>
          <Separator />
          <div className="flex-1 overflow-y-auto p-2">
            {isLoadingCollections && sidebarItems.length === 0 ? <p className="px-2 py-3 text-sm text-muted-foreground">Loading conversations...</p> : null}
            {!isLoadingCollections && sidebarItems.length === 0 ? (
              <div className="flex h-full flex-col items-center justify-center gap-2 p-5 text-center text-sm text-muted-foreground">
                <MessageSquare className="h-5 w-5" />
                Start a chat or run your first search.
              </div>
            ) : null}
            {sidebarItems.map((item) => {
              const active = selectedItem?.kind === item.kind && selectedItem.id === item.id;
              return (
                <button
                  key={`${item.kind}-${item.id}`}
                  className={cn(
                    "mb-1 w-full rounded-lg px-3 py-2 text-left transition",
                    active ? "bg-violet-500/30 text-violet-100" : "text-violet-200/85 hover:bg-violet-500/15"
                  )}
                  onClick={() => {
                    setContextMenu(null);
                    setDraftKind(null);
                    setSelectedItem({ kind: item.kind, id: item.id });
                  }}
                  onContextMenu={(event) => openContextMenu(event, { kind: item.kind, id: item.id })}
                  type="button"
                >
                  <div className="flex items-center gap-2">
                    {item.kind === "chat" ? <MessageSquare className="h-3.5 w-3.5" /> : <Search className="h-3.5 w-3.5" />}
                    <p className="truncate text-sm font-medium">{item.title}</p>
                  </div>
                  <p className={cn("mt-1 text-xs", active ? "text-violet-100/90" : "text-violet-300/60")}>{formatDate(item.updatedAt)}</p>
                </button>
              );
            })}
          </div>
        </aside>

        <main className="flex min-w-0 flex-1 flex-col">
          <header className="flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3">
            <div>
              <h1 className="text-sm font-semibold md:text-base">{selectedTitle}</h1>
              <p className="text-xs text-muted-foreground">
                Sybil Web{authMode ? ` (${authMode === "open" ? "open mode" : "token mode"})` : ""}
                {isSearchMode ? " • Exa Search" : ""}
              </p>
            </div>
            <div className="flex w-full max-w-xl items-center gap-2 md:w-auto">
              {!isSearchMode ? (
                <>
                  <select
                    className="h-9 rounded-md border border-input bg-background px-2 text-sm"
                    value={provider}
                    onChange={(event) => {
                      const nextProvider = event.currentTarget.value as Provider;
                      setProvider(nextProvider);
                      const options = getModelOptions(modelCatalog, nextProvider);
                      setModel((current) => pickProviderModel(options, providerModelPreferences[nextProvider], current));
                    }}
                    disabled={isSending}
                  >
                    <option value="openai">OpenAI</option>
                    <option value="anthropic">Anthropic</option>
                    <option value="xai">xAI</option>
                  </select>
                  <ModelCombobox
                    options={providerModelOptions}
                    value={model}
                    disabled={isSending || providerModelOptions.length === 0}
                    onChange={(nextModel) => {
                      setModel(nextModel);
                      setProviderModelPreferences((current) => ({
                        ...current,
                        [provider]: nextModel,
                      }));
                    }}
                  />
                </>
              ) : (
                <div className="flex h-9 items-center rounded-md border border-input px-3 text-sm text-muted-foreground">
                  <Globe2 className="mr-2 h-4 w-4" />
                  Search mode
                </div>
              )}
              <Button variant="outline" size="sm" onClick={handleLogout}>
                <LogOut className="mr-1 h-4 w-4" />
                Logout
              </Button>
            </div>
          </header>

          <div className="flex-1 overflow-y-auto px-3 py-6 md:px-10">
            {!isSearchMode ? (
              <ChatMessagesPanel messages={displayMessages} isLoading={isLoadingSelection} isSending={isSending} />
            ) : (
              <SearchResultsPanel search={selectedSearch} isLoading={isLoadingSelection} isRunning={isSearchRunning} />
            )}
            <div ref={transcriptEndRef} />
          </div>

          <footer className="border-t p-3 md:p-4">
            <div className="mx-auto max-w-3xl rounded-xl border bg-background p-2 shadow-sm">
              <Textarea
                id="composer-input"
                rows={1}
                value={composer}
                onInput={(event) => {
                  const textarea = event.currentTarget;
                  textarea.style.height = "0px";
                  textarea.style.height = `${textarea.scrollHeight}px`;
                  setComposer(textarea.value);
                }}
                onKeyDown={(event) => {
                  if (event.key === "Enter" && !event.shiftKey) {
                    event.preventDefault();
                    void handleSend();
                  }
                }}
                placeholder={isSearchMode ? "Search the web" : "Message Sybil"}
                className="max-h-40 min-h-0 resize-none overflow-y-auto border-0 shadow-none focus-visible:ring-0"
                disabled={isSending}
              />
              <div className={cn("flex items-center px-2 pb-1", error ? "justify-between" : "justify-end")}>
                {error ? <p className="text-xs text-red-600">{error}</p> : null}
                <Button onClick={() => void handleSend()} size="icon" disabled={isSending || !composer.trim()}>
                  {isSearchMode ? <Search className="h-4 w-4" /> : <SendHorizontal className="h-4 w-4" />}
                </Button>
              </div>
            </div>
          </footer>
        </main>
      </div>
      {contextMenu ? (
        <div
          ref={contextMenuRef}
          className="fixed z-50 min-w-40 rounded-md border border-border bg-background p-1 shadow-md"
          style={{ left: contextMenu.x, top: contextMenu.y }}
          onContextMenu={(event) => event.preventDefault()}
        >
          <button
            type="button"
            className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm text-red-600 transition hover:bg-muted disabled:text-muted-foreground"
            onClick={() => void handleDeleteFromContextMenu()}
            disabled={isSending}
          >
            <Trash2 className="h-3.5 w-3.5" />
            Delete
          </button>
        </div>
      ) : null}
    </div>
  );
}
