import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { Globe2, LogOut, MessageSquare, Plus, Search, SendHorizontal, ShieldCheck } from "lucide-preact";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Separator } from "@/components/ui/separator";
import {
  createChat,
  createSearch,
  getChat,
  getConfiguredToken,
  getSearch,
  listChats,
  listSearches,
  runCompletion,
  runSearch,
  setAuthToken,
  verifySession,
  type ChatDetail,
  type ChatSummary,
  type CompletionRequestMessage,
  type SearchDetail,
  type SearchResultItem,
  type SearchSummary,
} from "@/lib/api";
import { cn } from "@/lib/utils";

type Provider = "openai" | "anthropic" | "xai";
type AuthMode = "open" | "token";
type SidebarSelection = { kind: "chat" | "search"; id: string };
type SidebarItem = SidebarSelection & {
  title: string;
  updatedAt: string;
  createdAt: string;
};

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

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));
}

function readStoredToken() {
  return localStorage.getItem(TOKEN_STORAGE_KEY)?.trim() || null;
}

function persistToken(token: string | null) {
  if (token) {
    localStorage.setItem(TOKEN_STORAGE_KEY, token);
    return;
  }
  localStorage.removeItem(TOKEN_STORAGE_KEY);
}

function normalizeAuthError(message: string) {
  if (message.includes("missing bearer token") || message.includes("invalid bearer token")) {
    return "Authentication failed. Enter the ADMIN_TOKEN configured in server/.env.";
  }
  return message;
}

function summarizeResult(result: SearchResultItem) {
  const highlights = Array.isArray(result.highlights) ? result.highlights.filter(Boolean) : [];
  if (highlights.length) return highlights.join(" ").slice(0, 420);
  return (result.text ?? "").slice(0, 420);
}

function formatHost(url: string) {
  try {
    return new URL(url).hostname.replace(/^www\./, "");
  } catch {
    return url;
  }
}

export default function App() {
  const initialToken = readStoredToken() ?? getConfiguredToken() ?? "";

  const [authTokenInput, setAuthTokenInput] = useState(initialToken);
  const [isCheckingSession, setIsCheckingSession] = useState(true);
  const [isSigningIn, setIsSigningIn] = useState(false);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [authMode, setAuthMode] = useState<AuthMode | null>(null);
  const [authError, setAuthError] = useState<string | null>(null);

  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 [isLoadingCollections, setIsLoadingCollections] = useState(false);
  const [isLoadingSelection, setIsLoadingSelection] = useState(false);
  const [isSending, setIsSending] = useState(false);
  const [composer, setComposer] = useState("");
  const [provider, setProvider] = useState<Provider>("openai");
  const [model, setModel] = useState(PROVIDER_DEFAULT_MODELS.openai);
  const [error, setError] = useState<string | null>(null);
  const transcriptEndRef = useRef<HTMLDivElement>(null);

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

  const completeSessionCheck = async (tokenCandidate: string | null) => {
    setAuthToken(tokenCandidate);
    const session = await verifySession();
    setIsAuthenticated(true);
    setAuthMode(session.mode);
    setAuthError(null);
    persistToken(tokenCandidate);
  };

  const handleAuthFailure = (message: string) => {
    setIsAuthenticated(false);
    setAuthMode(null);
    setAuthError(normalizeAuthError(message));
    setAuthToken(null);
    persistToken(null);
    setChats([]);
    setSearches([]);
    setSelectedItem(null);
    setSelectedChat(null);
    setSelectedSearch(null);
  };

  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 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(() => {
    const token = readStoredToken() ?? getConfiguredToken();
    void (async () => {
      try {
        await completeSessionCheck(token);
      } catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        handleAuthFailure(message);
      } finally {
        setIsCheckingSession(false);
      }
    })();
  }, []);

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

  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 (selectedItem?.kind === "search") return;
    transcriptEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
  }, [selectedChat?.messages.length, isSending, selectedItem?.kind]);

  const messages = selectedChat?.messages ?? [];

  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 (!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";
  }, [selectedChat, selectedChatSummary, selectedItem, selectedSearch, selectedSearchSummary]);

  const isSearchMode = selectedItem?.kind === "search";

  const handleCreateChat = async () => {
    setError(null);
    try {
      const chat = await createChat();
      setSelectedItem({ kind: "chat", id: chat.id });
      setSelectedChat({
        id: chat.id,
        title: chat.title,
        createdAt: chat.createdAt,
        updatedAt: chat.updatedAt,
        messages: [],
      });
      setSelectedSearch(null);
      await refreshCollections({ kind: "chat", id: chat.id });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    }
  };

  const handleCreateSearch = async () => {
    setError(null);
    try {
      const search = await createSearch();
      setSelectedItem({ kind: "search", id: search.id });
      setSelectedSearch({
        id: search.id,
        title: search.title,
        query: search.query,
        createdAt: search.createdAt,
        updatedAt: search.updatedAt,
        requestId: null,
        latencyMs: null,
        error: null,
        results: [],
      });
      setSelectedChat(null);
      await refreshCollections({ kind: "search", id: search.id });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    }
  };

  const handleSendChat = async (content: string) => {
    let chatId = selectedItem?.kind === "chat" ? selectedItem.id : null;

    if (!chatId) {
      const chat = await createChat();
      chatId = chat.id;
      setSelectedItem({ kind: "chat", id: chatId });
      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 optimisticUserMessage = {
      id: `temp-user-${Date.now()}`,
      createdAt: new Date().toISOString(),
      role: "user" as const,
      content,
      name: null,
    };

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

    setSelectedChat((current) => {
      if (!current || current.id !== chatId) return current;
      return {
        ...current,
        messages: [...current.messages, optimisticUserMessage, optimisticAssistantMessage],
      };
    });

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

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

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

  const handleSendSearch = async (query: string) => {
    let searchId = selectedItem?.kind === "search" ? selectedItem.id : null;

    if (!searchId) {
      const search = await createSearch();
      searchId = search.id;
      setSelectedItem({ kind: "search", id: searchId });
    }

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

    setSelectedSearch((current) => {
      if (!current || current.id !== searchId) return current;
      return {
        ...current,
        title: query.slice(0, 80),
        query,
        error: null,
        results: [],
      };
    });

    const search = await runSearch(searchId, {
      query,
      title: query.slice(0, 80),
      type: "auto",
      numResults: 10,
    });

    setSelectedSearch(search);
    setSelectedChat(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 (selectedItem?.kind === "chat") {
        await refreshChat(selectedItem.id);
      }
      if (selectedItem?.kind === "search") {
        await refreshSearch(selectedItem.id);
      }
    } finally {
      setIsSending(false);
    }
  };

  const handleSignIn = async (tokenCandidate: string | null) => {
    setIsSigningIn(true);
    setAuthError(null);
    try {
      await completeSessionCheck(tokenCandidate);
      await refreshCollections();
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      setAuthError(normalizeAuthError(message));
      setIsAuthenticated(false);
      setAuthMode(null);
    } finally {
      setIsSigningIn(false);
    }
  };

  const handleLogout = () => {
    setAuthToken(null);
    persistToken(null);
    setIsAuthenticated(false);
    setAuthMode(null);
    setAuthError(null);
    setChats([]);
    setSearches([]);
    setSelectedItem(null);
    setSelectedChat(null);
    setSelectedSearch(null);
    setComposer("");
    setError(null);
  };

  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 (
      <div className="flex h-full items-center justify-center bg-[radial-gradient(circle_at_top,#171f33_0%,#111827_45%,#0b1020_100%)] p-4">
        <div className="w-full max-w-md rounded-2xl border bg-[#0f172a] p-6 shadow-xl shadow-black/40">
          <div className="mb-5 flex items-start gap-3">
            <div className="rounded-lg bg-slate-200 p-2 text-slate-900">
              <ShieldCheck className="h-4 w-4" />
            </div>
            <div>
              <h1 className="text-lg font-semibold">Sign in to Sybil</h1>
              <p className="mt-1 text-sm text-muted-foreground">Use your backend admin token.</p>
            </div>
          </div>

          <form
            className="space-y-3"
            onSubmit={(event) => {
              event.preventDefault();
              void handleSignIn(authTokenInput.trim() || null);
            }}
          >
            <Input
              type="password"
              autoComplete="off"
              placeholder="ADMIN_TOKEN"
              value={authTokenInput}
              onInput={(event) => setAuthTokenInput(event.currentTarget.value)}
              disabled={isSigningIn}
            />
            <Button className="w-full" type="submit" disabled={isSigningIn}>
              {isSigningIn ? "Signing in..." : "Sign in"}
            </Button>
            <Button
              className="w-full"
              type="button"
              variant="secondary"
              disabled={isSigningIn}
              onClick={() => void handleSignIn(null)}
            >
              Continue without token
            </Button>
          </form>

          {authError ? <p className="mt-3 text-sm text-red-600">{authError}</p> : null}
          <p className="mt-3 text-xs text-muted-foreground">If `ADMIN_TOKEN` is set in `/server/.env`, token login is required.</p>
        </div>
      </div>
    );
  }

  return (
    <div className="h-full p-3 md:p-5">
      <div className="mx-auto flex h-full w-full max-w-[1560px] overflow-hidden rounded-2xl border bg-background shadow-xl shadow-black/40">
        <aside className="flex w-80 shrink-0 flex-col border-r bg-[#111827]">
          <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-slate-700 text-slate-50" : "text-slate-200 hover:bg-slate-800"
                  )}
                  onClick={() => setSelectedItem({ 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-slate-200" : "text-slate-400")}>{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);
                      setModel(PROVIDER_DEFAULT_MODELS[nextProvider]);
                    }}
                    disabled={isSending}
                  >
                    <option value="openai">OpenAI</option>
                    <option value="anthropic">Anthropic</option>
                    <option value="xai">xAI</option>
                  </select>
                  <Input
                    value={model}
                    onInput={(event) => setModel(event.currentTarget.value)}
                    placeholder="Model"
                    disabled={isSending}
                  />
                </>
              ) : (
                <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 ? (
              <>
                {isLoadingSelection && messages.length === 0 ? <p className="text-sm text-muted-foreground">Loading messages...</p> : null}
                {!isLoadingSelection && messages.length === 0 ? (
                  <div className="mx-auto flex max-w-3xl flex-col items-center gap-3 rounded-xl border border-dashed p-8 text-center">
                    <h2 className="text-lg font-semibold">How can I help today?</h2>
                    <p className="text-sm text-muted-foreground">Ask a question to begin this conversation.</p>
                  </div>
                ) : null}
                <div className="mx-auto max-w-3xl space-y-6">
                  {messages.map((message) => {
                    const isUser = message.role === "user";
                    const isPendingAssistant = message.id.startsWith("temp-assistant-") && isSending;
                    return (
                      <div key={message.id} className={cn("flex", isUser ? "justify-end" : "justify-start")}>
                        <div
                          className={cn(
                            "max-w-[85%] whitespace-pre-wrap rounded-2xl px-4 py-3 text-sm leading-6",
                            isUser ? "bg-slate-200 text-slate-900" : "bg-slate-800 text-slate-100"
                          )}
                        >
                          {isPendingAssistant ? "Thinking..." : message.content}
                        </div>
                      </div>
                    );
                  })}
                </div>
              </>
            ) : (
              <div className="mx-auto w-full max-w-4xl">
                {selectedSearch?.query ? (
                  <div className="mb-5">
                    <p className="text-sm text-muted-foreground">Results for</p>
                    <h2 className="mt-1 text-xl font-semibold">{selectedSearch.query}</h2>
                    <p className="mt-1 text-xs text-muted-foreground">
                      {selectedSearch.results.length} result{selectedSearch.results.length === 1 ? "" : "s"}
                      {selectedSearch.latencyMs ? ` • ${selectedSearch.latencyMs} ms` : ""}
                    </p>
                  </div>
                ) : null}

                {isLoadingSelection && !selectedSearch?.results.length ? (
                  <p className="text-sm text-muted-foreground">Loading search...</p>
                ) : null}

                {!isLoadingSelection && !selectedSearch?.query ? (
                  <div className="flex flex-col items-center justify-center gap-2 rounded-xl border border-dashed p-8 text-center">
                    <Search className="h-6 w-6 text-muted-foreground" />
                    <h2 className="text-lg font-semibold">Search the web</h2>
                    <p className="text-sm text-muted-foreground">Use the composer below to run a new Exa search.</p>
                  </div>
                ) : null}

                {!isLoadingSelection && !!selectedSearch?.query && selectedSearch.results.length === 0 ? (
                  <p className="text-sm text-muted-foreground">No results found.</p>
                ) : null}

                <div className="space-y-6">
                  {selectedSearch?.results.map((result) => {
                    const summary = summarizeResult(result);
                    return (
                      <article key={result.id} className="rounded-lg border border-border bg-[#0d1322] px-4 py-4 shadow-sm">
                        <p className="text-xs text-emerald-300/85">{formatHost(result.url)}</p>
                        <a
                          href={result.url}
                          target="_blank"
                          rel="noreferrer"
                          className="mt-1 block text-lg font-medium text-sky-300 hover:underline"
                        >
                          {result.title || result.url}
                        </a>
                        {(result.publishedDate || result.author) && (
                          <p className="mt-1 text-xs text-muted-foreground">
                            {[result.publishedDate, result.author].filter(Boolean).join(" • ")}
                          </p>
                        )}
                        {summary ? <p className="mt-2 whitespace-pre-wrap text-sm leading-6 text-slate-200">{summary}</p> : null}
                      </article>
                    );
                  })}
                </div>

                {selectedSearch?.error ? <p className="mt-4 text-sm text-red-600">{selectedSearch.error}</p> : null}
              </div>
            )}
            <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
                rows={3}
                value={composer}
                onInput={(event) => setComposer(event.currentTarget.value)}
                onKeyDown={(event) => {
                  if (event.key === "Enter" && !event.shiftKey) {
                    event.preventDefault();
                    void handleSend();
                  }
                }}
                placeholder={isSearchMode ? "Search the web" : "Message Sybil"}
                className="resize-none border-0 shadow-none focus-visible:ring-0"
                disabled={isSending}
              />
              <div className="flex items-center justify-between px-2 pb-1">
                {error ? (
                  <p className="text-xs text-red-600">{error}</p>
                ) : (
                  <span className="text-xs text-muted-foreground">{isSearchMode ? "Enter to search" : "Enter to send"}</span>
                )}
                <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>
    </div>
  );
}
