import { useEffect, useRef, useState, type KeyboardEvent, type PointerEvent } from "react";
import { attachFile, editMessage, requestUploadUrl, sendMessageWithClientId, uploadToPresignedUrl } from "../api/chats";
import { useAuthStore } from "../store/authStore";
import { useChatStore } from "../store/chatStore";
import { buildWsUrl } from "../utils/ws";
import { getAppPreferences } from "../utils/preferences";

type RecordingState = "idle" | "recording" | "locked";

const STICKER_PRESETS: Array<{ name: string; url: string }> = [
  { name: "Thumbs Up", url: "https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/72x72/1f44d.png" },
  { name: "Party", url: "https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/72x72/1f389.png" },
  { name: "Fire", url: "https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/72x72/1f525.png" },
  { name: "Heart", url: "https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/72x72/2764.png" },
  { name: "Cool", url: "https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/72x72/1f60e.png" },
  { name: "Rocket", url: "https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/72x72/1f680.png" },
  { name: "Clap", url: "https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/72x72/1f44f.png" },
  { name: "Star", url: "https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/72x72/2b50.png" },
];

const GIF_PRESETS: Array<{ name: string; url: string }> = [
  { name: "Cat Typing", url: "https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" },
  { name: "Thumbs Up", url: "https://media.giphy.com/media/111ebonMs90YLu/giphy.gif" },
  { name: "Excited", url: "https://media.giphy.com/media/5VKbvrjxpVJCM/giphy.gif" },
  { name: "Dance", url: "https://media.giphy.com/media/l0MYt5jPR6QX5pnqM/giphy.gif" },
  { name: "Nice", url: "https://media.giphy.com/media/3o7abKhOpu0NwenH3O/giphy.gif" },
  { name: "Wow", url: "https://media.giphy.com/media/3oEjI6SIIHBdRxXI40/giphy.gif" },
  { name: "Thanks", url: "https://media.giphy.com/media/26ufdipQqU2lhNA4g/giphy.gif" },
  { name: "Success", url: "https://media.giphy.com/media/4T7e4DmcrP9du/giphy.gif" },
];

const STICKER_FAVORITES_KEY = "bm_sticker_favorites_v1";
const GIF_FAVORITES_KEY = "bm_gif_favorites_v1";
const GIF_PROVIDER = (import.meta.env.VITE_GIF_PROVIDER ?? "").toLowerCase();
const TENOR_API_KEY = (import.meta.env.VITE_TENOR_API_KEY ?? "").trim();
const TENOR_CLIENT_KEY = (import.meta.env.VITE_TENOR_CLIENT_KEY ?? "benya_messenger_web").trim();
const GIPHY_API_KEY = (import.meta.env.VITE_GIPHY_API_KEY ?? "").trim();
const GIF_SEARCH_ENABLED =
  (GIF_PROVIDER === "tenor" && TENOR_API_KEY.length > 0) ||
  (GIF_PROVIDER === "giphy" && GIPHY_API_KEY.length > 0);

function loadFavorites(key: string): Set<string> {
  try {
    const raw = window.localStorage.getItem(key);
    if (!raw) return new Set();
    const parsed = JSON.parse(raw) as string[];
    if (!Array.isArray(parsed)) return new Set();
    return new Set(parsed.filter((item) => typeof item === "string"));
  } catch {
    return new Set();
  }
}

function saveFavorites(key: string, values: Set<string>): void {
  try {
    window.localStorage.setItem(key, JSON.stringify([...values]));
  } catch {
    return;
  }
}

function pickSupportedAudioMimeType(): string | undefined {
  if (typeof MediaRecorder === "undefined" || typeof MediaRecorder.isTypeSupported !== "function") {
    return undefined;
  }
  const candidates = [
    "audio/ogg;codecs=opus",
    "audio/mp4",
    "audio/webm;codecs=opus",
    "audio/webm",
  ];
  for (const mime of candidates) {
    if (MediaRecorder.isTypeSupported(mime)) {
      return mime;
    }
  }
  return undefined;
}

export function MessageComposer() {
  const activeChatId = useChatStore((s) => s.activeChatId);
  const chats = useChatStore((s) => s.chats);
  const me = useAuthStore((s) => s.me);
  const draftsByChat = useChatStore((s) => s.draftsByChat);
  const setDraft = useChatStore((s) => s.setDraft);
  const clearDraft = useChatStore((s) => s.clearDraft);
  const addOptimisticMessage = useChatStore((s) => s.addOptimisticMessage);
  const confirmMessageByClientId = useChatStore((s) => s.confirmMessageByClientId);
  const removeOptimisticMessage = useChatStore((s) => s.removeOptimisticMessage);
  const replyToByChat = useChatStore((s) => s.replyToByChat);
  const setReplyToMessage = useChatStore((s) => s.setReplyToMessage);
  const editingByChat = useChatStore((s) => s.editingByChat);
  const setEditingMessage = useChatStore((s) => s.setEditingMessage);
  const upsertMessage = useChatStore((s) => s.upsertMessage);
  const accessToken = useAuthStore((s) => s.accessToken);

  const [text, setText] = useState("");
  const wsRef = useRef<WebSocket | null>(null);
  const wsTokenRef = useRef<string | null>(null);
  const recorderRef = useRef<MediaRecorder | null>(null);
  const recordingStreamRef = useRef<MediaStream | null>(null);
  const chunksRef = useRef<BlobPart[]>([]);
  const sendVoiceOnStopRef = useRef<boolean>(true);
  const recordingStartedAtRef = useRef<number | null>(null);
  const pointerStartYRef = useRef<number>(0);
  const pointerStartXRef = useRef<number>(0);
  const pointerCancelArmedRef = useRef<boolean>(false);
  const pointerMoveHandlerRef = useRef<((event: globalThis.PointerEvent) => void) | null>(null);
  const pointerUpHandlerRef = useRef<((event: globalThis.PointerEvent) => void) | null>(null);
  const recordingStateRef = useRef<RecordingState>("idle");
  const lastEditingMessageIdRef = useRef<number | null>(null);
  const prevActiveChatIdRef = useRef<number | null>(null);
  const typingActiveRef = useRef(false);
  const typingStopTimerRef = useRef<number | null>(null);

  const [isUploading, setIsUploading] = useState(false);
  const [uploadProgress, setUploadProgress] = useState(0);
  const [uploadError, setUploadError] = useState<string | null>(null);
  const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
  const [selectedType, setSelectedType] = useState<"file" | "image" | "video" | "audio">("file");
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
  const [showAttachMenu, setShowAttachMenu] = useState(false);
  const [showFormatMenu, setShowFormatMenu] = useState(false);
  const [showStickerMenu, setShowStickerMenu] = useState(false);
  const [showGifMenu, setShowGifMenu] = useState(false);
  const [stickerTab, setStickerTab] = useState<"all" | "favorites">("all");
  const [stickerQuery, setStickerQuery] = useState("");
  const [gifTab, setGifTab] = useState<"all" | "favorites">("all");
  const [gifQuery, setGifQuery] = useState("");
  const [gifResults, setGifResults] = useState<Array<{ name: string; url: string }>>([]);
  const [gifLoading, setGifLoading] = useState(false);
  const [gifSearchError, setGifSearchError] = useState<string | null>(null);
  const [favoriteStickers, setFavoriteStickers] = useState<Set<string>>(() => loadFavorites(STICKER_FAVORITES_KEY));
  const [favoriteGifs, setFavoriteGifs] = useState<Set<string>>(() => loadFavorites(GIF_FAVORITES_KEY));
  const [captionDraft, setCaptionDraft] = useState("");
  const mediaInputRef = useRef<HTMLInputElement | null>(null);
  const fileInputRef = useRef<HTMLInputElement | null>(null);
  const textareaRef = useRef<HTMLTextAreaElement | null>(null);

  const [recordingState, setRecordingState] = useState<RecordingState>("idle");
  const [recordSeconds, setRecordSeconds] = useState(0);
  const [dragHint, setDragHint] = useState<"idle" | "lock" | "cancel">("idle");
  const hasTextToSend = text.trim().length > 0;
  const activeChat = chats.find((chat) => chat.id === activeChatId);
  const editingMessage = activeChatId ? (editingByChat[activeChatId] ?? null) : null;
  const canSendInActiveChat = Boolean(
    activeChatId &&
      activeChat &&
      (activeChat.type !== "channel" || activeChat.my_role === "owner" || activeChat.my_role === "admin" || activeChat.is_saved)
  );

  async function searchGifs(term: string): Promise<Array<{ name: string; url: string }>> {
    if (GIF_PROVIDER === "tenor") {
      const params = new URLSearchParams({
        q: term,
        key: TENOR_API_KEY,
        client_key: TENOR_CLIENT_KEY,
        limit: "24",
        media_filter: "gif",
        contentfilter: "medium",
      });
      const response = await fetch(`https://tenor.googleapis.com/v2/search?${params.toString()}`);
      if (!response.ok) {
        throw new Error("tenor search failed");
      }
      const data = (await response.json()) as {
        results?: Array<{ content_description?: string; media_formats?: { gif?: { url?: string } } }>;
      };
      return (
        data.results
          ?.map((item) => ({
            name: item.content_description || "GIF",
            url: item.media_formats?.gif?.url || "",
          }))
          .filter((item) => item.url.length > 0) ?? []
      );
    }

    if (GIF_PROVIDER === "giphy") {
      const params = new URLSearchParams({
        api_key: GIPHY_API_KEY,
        q: term,
        limit: "24",
        rating: "pg-13",
        lang: "en",
      });
      const response = await fetch(`https://api.giphy.com/v1/gifs/search?${params.toString()}`);
      if (!response.ok) {
        throw new Error("giphy search failed");
      }
      const data = (await response.json()) as {
        data?: Array<{
          title?: string;
          images?: {
            fixed_height?: { url?: string };
            original?: { url?: string };
          };
        }>;
      };
      return (
        data.data
          ?.map((item) => ({
            name: item.title || "GIF",
            url: item.images?.fixed_height?.url || item.images?.original?.url || "",
          }))
          .filter((item) => item.url.length > 0) ?? []
      );
    }

    return [];
  }

  useEffect(() => {
    recordingStateRef.current = recordingState;
  }, [recordingState]);

  useEffect(() => {
    if (!activeChatId) {
      if (text !== "") {
        setText("");
      }
      lastEditingMessageIdRef.current = null;
      return;
    }
    if (editingMessage) {
      if (lastEditingMessageIdRef.current !== editingMessage.id) {
        setText(editingMessage.text ?? "");
      }
      lastEditingMessageIdRef.current = editingMessage.id;
      return;
    }
    lastEditingMessageIdRef.current = null;
    const draft = draftsByChat[activeChatId] ?? "";
    if (draft !== text) {
      setText(draft);
    }
  }, [activeChatId, draftsByChat, editingMessage, text]);

  useEffect(() => {
    return () => {
      if (previewUrl) {
        URL.revokeObjectURL(previewUrl);
      }
      if (recordingStateRef.current !== "idle") {
        stopRecord(false);
      }
      if (pointerMoveHandlerRef.current) {
        window.removeEventListener("pointermove", pointerMoveHandlerRef.current);
      }
      if (pointerUpHandlerRef.current) {
        window.removeEventListener("pointerup", pointerUpHandlerRef.current);
      }
      if (typingStopTimerRef.current !== null) {
        window.clearTimeout(typingStopTimerRef.current);
      }
      wsRef.current?.close();
      wsRef.current = null;
      wsTokenRef.current = null;
    };
  }, [previewUrl]);

  useEffect(() => {
    const activeToken = accessToken ?? null;
    if (wsRef.current && wsTokenRef.current !== activeToken) {
      wsRef.current.close();
      wsRef.current = null;
    }
    wsTokenRef.current = activeToken;
  }, [accessToken]);

  useEffect(() => {
    if (!activeChatId && recordingStateRef.current !== "idle") {
      stopRecord(false);
    }
  }, [activeChatId]);

  useEffect(() => {
    const prev = prevActiveChatIdRef.current;
    if (prev && prev !== activeChatId) {
      sendWsEvent("typing_stop", { chat_id: prev });
      typingActiveRef.current = false;
      if (typingStopTimerRef.current !== null) {
        window.clearTimeout(typingStopTimerRef.current);
        typingStopTimerRef.current = null;
      }
    }
    prevActiveChatIdRef.current = activeChatId;
  }, [activeChatId]);

  useEffect(() => {
    const onVisibility = () => {
      if (document.visibilityState === "hidden" && recordingStateRef.current === "recording") {
        setRecordingState("locked");
      }
    };
    const onPageHide = () => {
      if (recordingStateRef.current !== "idle") {
        stopRecord(false);
      }
    };
    document.addEventListener("visibilitychange", onVisibility);
    window.addEventListener("pagehide", onPageHide);
    return () => {
      document.removeEventListener("visibilitychange", onVisibility);
      window.removeEventListener("pagehide", onPageHide);
    };
  }, []);

  useEffect(() => {
    if (recordingState === "idle") {
      return;
    }
    const interval = window.setInterval(() => {
      if (!recordingStartedAtRef.current) {
        return;
      }
      const sec = Math.max(0, Math.floor((Date.now() - recordingStartedAtRef.current) / 1000));
      setRecordSeconds(sec);
    }, 250);
    return () => window.clearInterval(interval);
  }, [recordingState]);

  function makeClientMessageId(): string {
    if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
      return crypto.randomUUID();
    }
    return `msg-${Date.now()}-${Math.random().toString(16).slice(2)}`;
  }

  function getWs(): WebSocket | null {
    if (!accessToken || !activeChatId) {
      return null;
    }
    if (wsRef.current && wsTokenRef.current !== accessToken) {
      wsRef.current.close();
      wsRef.current = null;
    }
    if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
      return wsRef.current;
    }
    const wsUrl = buildWsUrl(accessToken);
    wsRef.current = new WebSocket(wsUrl);
    wsTokenRef.current = accessToken;
    return wsRef.current;
  }

  function sendWsEvent(eventName: string, payload: Record<string, unknown>) {
    const ws = getWs();
    if (!ws || ws.readyState !== WebSocket.OPEN) {
      return;
    }
    try {
      ws.send(JSON.stringify({ event: eventName, payload }));
    } catch {
      return;
    }
  }

  function sendRealtimeChatEvent(
    eventName:
      | "typing_start"
      | "typing_stop"
      | "recording_voice_start"
      | "recording_voice_stop"
  ) {
    if (!activeChatId) {
      return;
    }
    sendWsEvent(eventName, { chat_id: activeChatId });
  }

  function emitTypingStopIfActive() {
    if (!activeChatId) {
      return;
    }
    if (typingStopTimerRef.current !== null) {
      window.clearTimeout(typingStopTimerRef.current);
      typingStopTimerRef.current = null;
    }
    if (typingActiveRef.current) {
      sendRealtimeChatEvent("typing_stop");
      typingActiveRef.current = false;
    }
  }

  function syncTypingForText(nextText: string) {
    if (!activeChatId) {
      return;
    }
    const hasText = nextText.trim().length > 0;
    if (!hasText) {
      emitTypingStopIfActive();
      return;
    }
    if (!typingActiveRef.current) {
      sendRealtimeChatEvent("typing_start");
      typingActiveRef.current = true;
    }
    if (typingStopTimerRef.current !== null) {
      window.clearTimeout(typingStopTimerRef.current);
    }
    typingStopTimerRef.current = window.setTimeout(() => {
      if (!typingActiveRef.current) {
        return;
      }
      sendRealtimeChatEvent("typing_stop");
      typingActiveRef.current = false;
      typingStopTimerRef.current = null;
    }, 2500);
  }

  async function handleSend() {
    if (!activeChatId || !text.trim() || !me || !canSendInActiveChat) {
      return;
    }
    if (editingMessage) {
      try {
        const updated = await editMessage(editingMessage.id, text.trim());
        upsertMessage(activeChatId, updated);
        setEditingMessage(activeChatId, null);
        setText("");
        clearDraft(activeChatId);
      } catch {
        setUploadError("Edit failed. Message may be older than 7 days.");
      }
      return;
    }
    const clientMessageId = makeClientMessageId();
    const textValue = text.trim();
    const replyToMessageId = activeChatId ? (replyToByChat[activeChatId]?.id ?? undefined) : undefined;
    addOptimisticMessage({ chatId: activeChatId, senderId: me.id, type: "text", text: textValue, clientMessageId });
    try {
      let message = null;
      for (let attempt = 0; attempt < 2; attempt += 1) {
        try {
          message = await sendMessageWithClientId(activeChatId, textValue, "text", clientMessageId, replyToMessageId);
          break;
        } catch (error) {
          if (attempt === 1) {
            throw error;
          }
          await new Promise((resolve) => window.setTimeout(resolve, 250));
        }
      }
      if (!message) {
        throw new Error("send failed");
      }
      confirmMessageByClientId(activeChatId, clientMessageId, message);
      setText("");
      clearDraft(activeChatId);
      setReplyToMessage(activeChatId, null);
      emitTypingStopIfActive();
    } catch {
      removeOptimisticMessage(activeChatId, clientMessageId);
      setUploadError("Message send failed. Please try again.");
    }
  }

  async function sendPresetMedia(url: string) {
    if (!activeChatId || !me || !canSendInActiveChat) {
      return;
    }
    const clientMessageId = makeClientMessageId();
    const replyToMessageId = activeChatId ? (replyToByChat[activeChatId]?.id ?? undefined) : undefined;
    addOptimisticMessage({ chatId: activeChatId, senderId: me.id, type: "image", text: url, clientMessageId });
    try {
      const message = await sendMessageWithClientId(activeChatId, url, "image", clientMessageId, replyToMessageId);
      confirmMessageByClientId(activeChatId, clientMessageId, message);
      setReplyToMessage(activeChatId, null);
      setShowStickerMenu(false);
      setShowGifMenu(false);
      setGifQuery("");
    } catch {
      removeOptimisticMessage(activeChatId, clientMessageId);
      setUploadError("Failed to send media");
    }
  }

  function toggleStickerFavorite(url: string) {
    setFavoriteStickers((prev) => {
      const next = new Set(prev);
      if (next.has(url)) {
        next.delete(url);
      } else {
        next.add(url);
      }
      saveFavorites(STICKER_FAVORITES_KEY, next);
      return next;
    });
  }

  function toggleGifFavorite(url: string) {
    setFavoriteGifs((prev) => {
      const next = new Set(prev);
      if (next.has(url)) {
        next.delete(url);
      } else {
        next.add(url);
      }
      saveFavorites(GIF_FAVORITES_KEY, next);
      return next;
    });
  }

  useEffect(() => {
    const term = gifQuery.trim();
    if (!showGifMenu || term.length < 2) {
      setGifResults([]);
      setGifLoading(false);
      setGifSearchError(null);
      return;
    }
    if (!GIF_SEARCH_ENABLED) {
      setGifResults([]);
      setGifLoading(false);
      setGifSearchError("GIF search is not configured. Using preset GIFs.");
      return;
    }
    let cancelled = false;
    const timer = window.setTimeout(() => {
      setGifLoading(true);
      setGifSearchError(null);
      void (async () => {
        try {
          const rows = await searchGifs(term);
          if (cancelled) return;
          setGifResults(rows);
        } catch {
          if (!cancelled) {
            setGifResults([]);
            setGifSearchError("GIF search is unavailable right now. Using preset GIFs.");
          }
        } finally {
          if (!cancelled) {
            setGifLoading(false);
          }
        }
      })();
    }, 280);
    return () => {
      cancelled = true;
      window.clearTimeout(timer);
    };
  }, [gifQuery, showGifMenu]);

  function onComposerKeyDown(event: KeyboardEvent<HTMLTextAreaElement>) {
    const hasModifier = event.ctrlKey || event.metaKey;
    if (hasModifier) {
      const key = event.key.toLowerCase();
      if (key === "b") {
        event.preventDefault();
        insertFormatting("**", "**");
        return;
      }
      if (key === "i") {
        event.preventDefault();
        insertFormatting("*", "*");
        return;
      }
      if (key === "u") {
        event.preventDefault();
        insertFormatting("__", "__");
        return;
      }
      if (key === "k") {
        event.preventDefault();
        insertLink();
        return;
      }
      if (event.shiftKey && key === "x") {
        event.preventDefault();
        insertFormatting("~~", "~~");
        return;
      }
      if (event.shiftKey && (event.key === "`" || event.code === "Backquote")) {
        event.preventDefault();
        insertFormatting("`", "`");
        return;
      }
    }

    if (event.key !== "Enter") {
      return;
    }
    const prefs = getAppPreferences();
    const sendWithCtrlEnter = prefs.sendMode === "ctrl_enter";
    if (sendWithCtrlEnter) {
      if (event.ctrlKey) {
        event.preventDefault();
        void handleSend();
      }
      return;
    }
    if (!event.shiftKey) {
      event.preventDefault();
      void handleSend();
    }
  }

  async function handleUpload(file: File, messageType: "file" | "image" | "video" | "audio" | "voice" = "file", waveformPoints?: number[] | null) {
    if (!activeChatId || !me || !canSendInActiveChat) {
      return;
    }
    setIsUploading(true);
    setUploadProgress(0);
    setUploadError(null);
    const clientMessageId = makeClientMessageId();
    const replyToMessageId = activeChatId ? (replyToByChat[activeChatId]?.id ?? undefined) : undefined;
    try {
      const upload = await requestUploadUrl(file);
      await uploadToPresignedUrl(upload.upload_url, upload.required_headers, file, setUploadProgress);
      addOptimisticMessage({
        chatId: activeChatId,
        senderId: me.id,
        type: messageType,
        text: upload.file_url,
        clientMessageId,
      });
      const message = await sendMessageWithClientId(activeChatId, upload.file_url, messageType, clientMessageId, replyToMessageId);
      const messageWithWaveform = waveformPoints?.length ? { ...message, attachment_waveform: waveformPoints } : message;
      confirmMessageByClientId(activeChatId, clientMessageId, messageWithWaveform);
      try {
        await attachFile(
          message.id,
          upload.file_url,
          file.type || "application/octet-stream",
          file.size,
          waveformPoints ?? null
        );
      } catch {
        setUploadError("File sent, but metadata save failed. Please refresh chat.");
      }
      setReplyToMessage(activeChatId, null);
    } catch {
      removeOptimisticMessage(activeChatId, clientMessageId);
      setUploadError("Upload failed. Please try again.");
    } finally {
      setIsUploading(false);
    }
  }

  function inferType(file: File): "file" | "image" | "video" | "audio" {
    if (file.type.startsWith("image/")) return "image";
    if (file.type.startsWith("video/")) return "video";
    if (file.type.startsWith("audio/")) return "audio";
    return "file";
  }

  function loadImageFromFile(file: File): Promise<HTMLImageElement> {
    return new Promise((resolve, reject) => {
      const image = new Image();
      const url = URL.createObjectURL(file);
      image.onload = () => {
        URL.revokeObjectURL(url);
        resolve(image);
      };
      image.onerror = () => {
        URL.revokeObjectURL(url);
        reject(new Error("Failed to load image"));
      };
      image.src = url;
    });
  }

  async function compressImageForWeb(file: File): Promise<File> {
    const image = await loadImageFromFile(file);
    const maxSide = 1920;
    const ratio = Math.min(1, maxSide / Math.max(image.width, image.height));
    const width = Math.max(1, Math.round(image.width * ratio));
    const height = Math.max(1, Math.round(image.height * ratio));

    const canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext("2d");
    if (!ctx) {
      return file;
    }
    ctx.drawImage(image, 0, 0, width, height);

    const blob = await new Promise<Blob | null>((resolve) => {
      canvas.toBlob((result) => resolve(result), "image/jpeg", 0.82);
    });
    if (!blob || blob.size >= file.size) {
      return file;
    }
    const baseName = file.name.replace(/\.[^/.]+$/, "");
    return new File([blob], `${baseName || "image"}-web.jpg`, { type: "image/jpeg" });
  }

  async function prepareFileForUpload(file: File, fileType: "file" | "image" | "video" | "audio"): Promise<File> {
    if (fileType === "image") {
      return compressImageForWeb(file);
    }
    return file;
  }

  async function startRecord() {
    if (recordingState !== "idle" || !canSendInActiveChat) {
      return false;
    }
    try {
      if (!("mediaDevices" in navigator) || !navigator.mediaDevices.getUserMedia) {
        setUploadError("Microphone is not supported in this browser.");
        return false;
      }
      if (navigator.permissions && navigator.permissions.query) {
        const permission = await navigator.permissions.query({ name: "microphone" as PermissionName });
        if (permission.state === "denied") {
          setUploadError("Microphone access denied. Allow microphone in browser site permissions.");
          return false;
        }
      }
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      const mimeType = pickSupportedAudioMimeType();
      const recorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream);
      recordingStreamRef.current = stream;
      chunksRef.current = [];
      sendVoiceOnStopRef.current = true;
      recorder.ondataavailable = (event) => chunksRef.current.push(event.data);
      recorder.onstop = async () => {
        const shouldSend = sendVoiceOnStopRef.current;
        const durationMs = recordingStartedAtRef.current ? Date.now() - recordingStartedAtRef.current : 0;
        const data = [...chunksRef.current];
        chunksRef.current = [];
        if (recordingStreamRef.current) {
          recordingStreamRef.current.getTracks().forEach((track) => track.stop());
          recordingStreamRef.current = null;
        }
        recordingStartedAtRef.current = null;
        if (!shouldSend || data.length === 0) {
          return;
        }
        if (durationMs < 1000) {
          setUploadError("Voice message is too short. Minimum length is 1 second.");
          return;
        }
        const outputMime = recorder.mimeType || mimeType || "audio/webm";
        const blob = new Blob(data, { type: outputMime });
        if (blob.size < 512) {
          setUploadError("Voice message is empty. Please try recording again.");
          return;
        }
        const ext = outputMime.includes("ogg") ? "ogg" : outputMime.includes("mp4") ? "m4a" : "webm";
        const file = new File([blob], `voice-${Date.now()}.${ext}`, { type: outputMime });
        const waveform = await buildWaveformPoints(blob, 64);
        await handleUpload(file, "voice", waveform);
      };
      recorderRef.current = recorder;
      recorder.start(250);
      recordingStartedAtRef.current = Date.now();
      setRecordSeconds(0);
      setRecordingState("recording");
      emitTypingStopIfActive();
      sendRealtimeChatEvent("recording_voice_start");
      return true;
    } catch {
      setUploadError("Microphone access denied. Please allow microphone and retry.");
      return false;
    }
  }

  function stopRecord(send: boolean) {
    if (recordingStateRef.current !== "idle") {
      sendRealtimeChatEvent("recording_voice_stop");
    }
    sendVoiceOnStopRef.current = send;
    pointerCancelArmedRef.current = false;
    setDragHint("idle");
    if (recorderRef.current && recorderRef.current.state !== "inactive") {
      try {
        recorderRef.current.stop();
      } catch {
        // ignore recorder lifecycle race during quick chat switches/unmount
      }
    }
    recorderRef.current = null;
    setRecordingState("idle");
    setRecordSeconds(0);
    if (pointerMoveHandlerRef.current) {
      window.removeEventListener("pointermove", pointerMoveHandlerRef.current);
      pointerMoveHandlerRef.current = null;
    }
    if (pointerUpHandlerRef.current) {
      window.removeEventListener("pointerup", pointerUpHandlerRef.current);
      pointerUpHandlerRef.current = null;
    }
  }

  async function onMicPointerDown(event: PointerEvent<HTMLButtonElement>) {
    event.preventDefault();
    const started = await startRecord();
    if (!started) {
      return;
    }
    pointerStartYRef.current = event.clientY;
    pointerStartXRef.current = event.clientX;
    pointerCancelArmedRef.current = false;
    setDragHint("idle");

    if (pointerMoveHandlerRef.current) {
      window.removeEventListener("pointermove", pointerMoveHandlerRef.current);
    }
    if (pointerUpHandlerRef.current) {
      window.removeEventListener("pointerup", pointerUpHandlerRef.current);
    }

    const onPointerMove = (moveEvent: globalThis.PointerEvent) => {
      const current = recordingStateRef.current;
      if (current === "idle") {
        return;
      }
      const deltaY = pointerStartYRef.current - moveEvent.clientY;
      const deltaX = pointerStartXRef.current - moveEvent.clientX;

      if (current === "recording" && deltaY > 70) {
        setRecordingState("locked");
        setDragHint("idle");
        return;
      }

      if (current === "recording") {
        if (deltaX > 90) {
          pointerCancelArmedRef.current = true;
          setDragHint("cancel");
        } else if (deltaY > 40) {
          pointerCancelArmedRef.current = false;
          setDragHint("lock");
        } else {
          pointerCancelArmedRef.current = false;
          setDragHint("idle");
        }
      }
    };

    const finishPointerSession = () => {
      if (pointerMoveHandlerRef.current) {
        window.removeEventListener("pointermove", pointerMoveHandlerRef.current);
      }
      if (pointerUpHandlerRef.current) {
        window.removeEventListener("pointerup", pointerUpHandlerRef.current);
      }
      pointerMoveHandlerRef.current = null;
      pointerUpHandlerRef.current = null;

      const current = recordingStateRef.current;
      if (current === "recording") {
        stopRecord(!pointerCancelArmedRef.current);
      }
      setDragHint("idle");
    };

    const onPointerUp = () => {
      finishPointerSession();
    };
    const onPointerCancel = () => {
      pointerCancelArmedRef.current = true;
      finishPointerSession();
    };

    pointerMoveHandlerRef.current = onPointerMove;
    pointerUpHandlerRef.current = onPointerUp;
    window.addEventListener("pointermove", onPointerMove);
    window.addEventListener("pointerup", onPointerUp);
    window.addEventListener("pointercancel", onPointerCancel, { once: true });
  }

  function selectFiles(files: File[]) {
    if (!files.length) {
      return;
    }
    setUploadError(null);
    setSelectedFiles(files);
    setShowAttachMenu(false);
    const fileType = inferType(files[0]);
    setSelectedType(fileType);
    if (previewUrl) {
      URL.revokeObjectURL(previewUrl);
    }
    if (files.length === 1 && (fileType === "image" || fileType === "video")) {
      setPreviewUrl(URL.createObjectURL(files[0]));
    } else {
      setPreviewUrl(null);
    }
  }

  async function sendSelectedFiles() {
    if (!selectedFiles.length || !activeChatId || !me) {
      return;
    }
    setIsUploading(true);
    setUploadError(null);
    setUploadProgress(0);
    const prepared = await Promise.all(
      selectedFiles.map(async (file) => {
        const kind = inferType(file);
        const uploadFile = await prepareFileForUpload(file, kind);
        return { file: uploadFile, kind };
      })
    );
    const uploaded: Array<{ fileUrl: string; fileType: string; fileSize: number; kind: "file" | "image" | "video" | "audio" }> = [];
    try {
      for (let index = 0; index < prepared.length; index += 1) {
        const current = prepared[index];
        const upload = await requestUploadUrl(current.file);
        await uploadToPresignedUrl(upload.upload_url, upload.required_headers, current.file, (percent) => {
          const done = index / prepared.length;
          const currentPart = (percent / 100) / prepared.length;
          setUploadProgress(Math.min(100, Math.round((done + currentPart) * 100)));
        });
        uploaded.push({
          fileUrl: upload.file_url,
          fileType: current.file.type || "application/octet-stream",
          fileSize: current.file.size,
          kind: current.kind,
        });
      }
      const kindSet = new Set(uploaded.map((item) => item.kind));
      let inferredType: "file" | "image" | "video" | "audio" = "file";
      if ([...kindSet].every((kind) => kind === "image")) {
        inferredType = "image";
      } else if ([...kindSet].every((kind) => kind === "image" || kind === "video")) {
        inferredType = "video";
      } else if ([...kindSet].every((kind) => kind === "audio")) {
        inferredType = "audio";
      } else if (kindSet.size === 1) {
        inferredType = uploaded[0].kind;
      }
      const messageType = inferredType;
      const caption = captionDraft.trim() || null;
      const clientMessageId = makeClientMessageId();
      addOptimisticMessage({
        chatId: activeChatId,
        senderId: me.id,
        type: messageType,
        text: caption,
        clientMessageId,
      });
      const replyToMessageId = replyToByChat[activeChatId]?.id ?? undefined;
      const created = await sendMessageWithClientId(
        activeChatId,
        caption,
        messageType,
        clientMessageId,
        replyToMessageId
      );
      confirmMessageByClientId(activeChatId, clientMessageId, created);
      for (const item of uploaded) {
        await attachFile(created.id, item.fileUrl, item.fileType, item.fileSize);
      }
      setReplyToMessage(activeChatId, null);
    } catch {
      setUploadError("Upload failed. Please try again.");
    } finally {
      setIsUploading(false);
      setUploadProgress(0);
    }
    if (previewUrl) {
      URL.revokeObjectURL(previewUrl);
    }
    setSelectedFiles([]);
    setPreviewUrl(null);
    setCaptionDraft("");
    setSelectedType("file");
  }

  function cancelSelectedFile() {
    if (previewUrl) {
      URL.revokeObjectURL(previewUrl);
    }
    setSelectedFiles([]);
    setPreviewUrl(null);
    setCaptionDraft("");
    setSelectedType("file");
    setUploadProgress(0);
    setUploadError(null);
  }

  function formatBytes(size: number): string {
    if (size < 1024) return `${size} B`;
    if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
    return `${(size / (1024 * 1024)).toFixed(1)} MB`;
  }

  function insertFormatting(startTag: string, endTag = startTag, placeholder = "text") {
    const textarea = textareaRef.current;
    if (!textarea) {
      return;
    }
    const start = textarea.selectionStart ?? text.length;
    const end = textarea.selectionEnd ?? text.length;
    const selected = text.slice(start, end);
    const middle = selected || placeholder;
    const nextValue = `${text.slice(0, start)}${startTag}${middle}${endTag}${text.slice(end)}`;
    setText(nextValue);
    if (activeChatId) {
      setDraft(activeChatId, nextValue);
    }
    requestAnimationFrame(() => {
      textarea.focus();
      if (selected) {
        const pos = start + startTag.length + middle.length + endTag.length;
        textarea.setSelectionRange(pos, pos);
      } else {
        const selStart = start + startTag.length;
        const selEnd = selStart + middle.length;
        textarea.setSelectionRange(selStart, selEnd);
      }
    });
  }

  function insertLink() {
    const textarea = textareaRef.current;
    if (!textarea) {
      return;
    }
    const start = textarea.selectionStart ?? text.length;
    const end = textarea.selectionEnd ?? text.length;
    const selected = text.slice(start, end).trim() || "text";
    const href = window.prompt("Enter URL (https://...)");
    if (!href) {
      return;
    }
    const link = `[${selected}](${href.trim()})`;
    const nextValue = `${text.slice(0, start)}${link}${text.slice(end)}`;
    setText(nextValue);
    if (activeChatId) {
      setDraft(activeChatId, nextValue);
    }
    requestAnimationFrame(() => {
      textarea.focus();
      const pos = start + link.length;
      textarea.setSelectionRange(pos, pos);
    });
  }

  function insertQuoteBlock() {
    const textarea = textareaRef.current;
    if (!textarea) {
      return;
    }
    const start = textarea.selectionStart ?? text.length;
    const end = textarea.selectionEnd ?? text.length;
    const selected = text.slice(start, end);
    const source = selected || "quote";
    const quoted = source
      .split("\n")
      .map((line) => `> ${line}`)
      .join("\n");
    const nextValue = `${text.slice(0, start)}${quoted}${text.slice(end)}`;
    setText(nextValue);
    if (activeChatId) {
      setDraft(activeChatId, nextValue);
    }
    requestAnimationFrame(() => {
      textarea.focus();
      const pos = start + quoted.length;
      textarea.setSelectionRange(pos, pos);
    });
  }

  return (
    <div className="border-t border-slate-700/50 bg-slate-900/55 p-3">
      {activeChatId && replyToByChat[activeChatId] ? (
        <div className="mb-2 flex items-start justify-between rounded-lg border border-slate-700/80 bg-slate-800/70 px-3 py-2 text-xs">
          <div className="min-w-0">
            <p className="font-semibold text-sky-300">Replying</p>
            <p className="truncate text-slate-300">{replyToByChat[activeChatId]?.text || "[media]"}</p>
          </div>
          <button className="ml-2 rounded bg-slate-700 px-2 py-1 text-[11px]" onClick={() => setReplyToMessage(activeChatId, null)}>
            Cancel
          </button>
        </div>
      ) : null}

      {activeChatId && editingMessage ? (
        <div className="mb-2 flex items-start justify-between rounded-lg border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs">
          <div className="min-w-0">
            <p className="font-semibold text-amber-200">Editing message</p>
            <p className="truncate text-amber-100/80">{editingMessage.text || "[empty]"}</p>
          </div>
          <button
            className="ml-2 rounded bg-slate-700 px-2 py-1 text-[11px]"
            onClick={() => {
              setEditingMessage(activeChatId, null);
              setText(draftsByChat[activeChatId] ?? "");
            }}
            type="button"
          >
            Cancel
          </button>
        </div>
      ) : null}

      {recordingState !== "idle" ? (
        <div className="mb-2 rounded-xl border border-slate-700/80 bg-slate-900/95 px-3 py-2 text-xs text-slate-200">
          <div className="flex items-center justify-between">
            <span>🎤 Recording {formatDuration(recordSeconds)}</span>
            {recordingState === "recording" ? (
              <span className={dragHint === "cancel" ? "text-red-300" : dragHint === "lock" ? "text-sky-300" : "text-slate-400"}>
                {dragHint === "cancel" ? "Release to cancel" : dragHint === "lock" ? "Release up to lock" : "Slide up to lock, left to cancel"}
              </span>
            ) : (
              <span className="text-sky-300">Locked</span>
            )}
          </div>
          {recordingState === "locked" ? (
            <div className="mt-2 flex gap-2">
              <button className="w-full rounded bg-slate-700 px-3 py-1.5" onClick={() => stopRecord(false)} type="button">
                Cancel
              </button>
              <button className="w-full rounded bg-sky-500 px-3 py-1.5 font-semibold text-slate-950" onClick={() => stopRecord(true)} type="button">
                Send Voice
              </button>
            </div>
          ) : null}
        </div>
      ) : null}

      {showFormatMenu ? (
        <div className="mb-2 flex items-center gap-1 rounded-2xl border border-slate-700/80 bg-slate-900/95 px-2 py-1.5">
          <button className="rounded px-2 py-1 text-xs hover:bg-slate-800" onClick={() => insertFormatting("||", "||")} type="button" title="Spoiler">
            👁
          </button>
          <button className="rounded px-2 py-1 text-xs font-semibold hover:bg-slate-800" onClick={() => insertFormatting("**", "**")} type="button" title="Bold">
            B
          </button>
          <button className="rounded px-2 py-1 text-xs italic hover:bg-slate-800" onClick={() => insertFormatting("*", "*")} type="button" title="Italic">
            I
          </button>
          <button className="rounded px-2 py-1 text-xs underline hover:bg-slate-800" onClick={() => insertFormatting("__", "__")} type="button" title="Underline">
            U
          </button>
          <button className="rounded px-2 py-1 text-xs line-through hover:bg-slate-800" onClick={() => insertFormatting("~~", "~~")} type="button" title="Strikethrough">
            S
          </button>
          <button className="rounded px-2 py-1 text-xs hover:bg-slate-800" onClick={() => insertFormatting("`", "`")} type="button" title="Monospace">
            M
          </button>
          <button className="rounded px-2 py-1 text-xs hover:bg-slate-800" onClick={() => insertFormatting("```\n", "\n```", "code")} type="button" title="Code block">
            {"</>"}
          </button>
          <button className="rounded px-2 py-1 text-xs hover:bg-slate-800" onClick={insertQuoteBlock} type="button" title="Quote">
            ❝
          </button>
          <button className="rounded px-2 py-1 text-xs hover:bg-slate-800" onClick={insertLink} type="button" title="Link">
            🔗
          </button>
        </div>
      ) : null}

      {showStickerMenu ? (
      <div className="mb-2 rounded-xl border border-slate-700/80 bg-slate-900/95 p-2">
          <div className="mb-2 flex items-center justify-between gap-2">
            <p className="text-xs font-semibold text-slate-200">Stickers</p>
            <input
              className="w-full max-w-xs rounded border border-slate-700/80 bg-slate-800/80 px-2 py-1 text-xs outline-none placeholder:text-slate-400 focus:border-sky-500"
              onChange={(event) => setStickerQuery(event.target.value)}
              placeholder="Search sticker"
              value={stickerQuery}
            />
            <div className="flex items-center gap-1">
              <button
                className={`rounded px-2 py-1 text-[11px] ${stickerTab === "all" ? "bg-sky-500 text-slate-950" : "bg-slate-700"}`}
                onClick={() => setStickerTab("all")}
                type="button"
              >
                All
              </button>
              <button
                className={`rounded px-2 py-1 text-[11px] ${stickerTab === "favorites" ? "bg-sky-500 text-slate-950" : "bg-slate-700"}`}
                onClick={() => setStickerTab("favorites")}
                type="button"
              >
                Favorites
              </button>
              <button className="rounded bg-slate-700 px-2 py-1 text-[11px]" onClick={() => setShowStickerMenu(false)} type="button">
                Close
              </button>
            </div>
          </div>
          <div className="grid grid-cols-4 gap-2 md:grid-cols-8">
            {STICKER_PRESETS
              .filter((item) => (stickerTab === "favorites" ? favoriteStickers.has(item.url) : true))
              .filter((item) => item.name.toLowerCase().includes(stickerQuery.trim().toLowerCase()))
              .map((sticker) => (
              <button
                className="relative rounded-lg bg-slate-800/80 p-2 hover:bg-slate-700"
                key={sticker.url}
                onClick={() => void sendPresetMedia(sticker.url)}
                title={sticker.name}
                type="button"
              >
                <img alt={sticker.name} className="mx-auto h-9 w-9 object-contain" draggable={false} src={sticker.url} />
                <span
                  className="absolute right-1 top-1 text-[10px]"
                  onClick={(event) => {
                    event.preventDefault();
                    event.stopPropagation();
                    toggleStickerFavorite(sticker.url);
                  }}
                >
                  {favoriteStickers.has(sticker.url) ? "★" : "☆"}
                </span>
              </button>
            ))}
          </div>
          {STICKER_PRESETS
            .filter((item) => (stickerTab === "favorites" ? favoriteStickers.has(item.url) : true))
            .filter((item) => item.name.toLowerCase().includes(stickerQuery.trim().toLowerCase())).length === 0 ? (
            <p className="mt-2 text-xs text-slate-400">No stickers found</p>
          ) : null}
        </div>
      ) : null}

      {showGifMenu ? (
        <div className="mb-2 rounded-xl border border-slate-700/80 bg-slate-900/95 p-2">
          <div className="mb-2 flex items-center justify-between gap-2">
            <p className="text-xs font-semibold text-slate-200">GIF</p>
            <input
              className="w-full max-w-xs rounded border border-slate-700/80 bg-slate-800/80 px-2 py-1 text-xs outline-none placeholder:text-slate-400 focus:border-sky-500"
              onChange={(event) => setGifQuery(event.target.value)}
              placeholder="Search GIF"
              value={gifQuery}
            />
            <button className="rounded bg-slate-700 px-2 py-1 text-[11px]" onClick={() => setShowGifMenu(false)} type="button">
              Close
            </button>
          </div>
          <div className="mb-2 flex items-center gap-1">
            <button
              className={`rounded px-2 py-1 text-[11px] ${gifTab === "all" ? "bg-sky-500 text-slate-950" : "bg-slate-700"}`}
              onClick={() => setGifTab("all")}
              type="button"
            >
              All
            </button>
            <button
              className={`rounded px-2 py-1 text-[11px] ${gifTab === "favorites" ? "bg-sky-500 text-slate-950" : "bg-slate-700"}`}
              onClick={() => setGifTab("favorites")}
              type="button"
            >
              Favorites
            </button>
          </div>
          <div className="grid grid-cols-2 gap-2 md:grid-cols-4">
            {(gifResults.length > 0 ? gifResults : GIF_PRESETS)
              .filter((item) => item.name.toLowerCase().includes(gifQuery.trim().toLowerCase()) || gifResults.length > 0)
              .filter((item) => (gifTab === "favorites" ? favoriteGifs.has(item.url) : true))
              .map((gif) => (
              <button
                className="relative overflow-hidden rounded-lg bg-slate-800/80 hover:bg-slate-700"
                key={gif.url}
                onClick={() => void sendPresetMedia(gif.url)}
                title={gif.name}
                type="button"
              >
                <img alt={gif.name} className="h-20 w-full object-cover md:h-24" draggable={false} src={gif.url} />
                <span
                  className="absolute right-1 top-1 rounded bg-black/45 px-1 text-[10px] text-white"
                  onClick={(event) => {
                    event.preventDefault();
                    event.stopPropagation();
                    toggleGifFavorite(gif.url);
                  }}
                >
                  {favoriteGifs.has(gif.url) ? "★" : "☆"}
                </span>
              </button>
            ))}
          </div>
          {gifLoading ? <p className="mt-2 text-xs text-slate-400">Searching GIF...</p> : null}
          {gifSearchError ? <p className="mt-2 text-xs text-amber-300">{gifSearchError}</p> : null}
        </div>
      ) : null}

      {!canSendInActiveChat && activeChat?.type === "channel" ? (
        <div className="mb-2 rounded-xl border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-200">
          Read-only channel: only owners and admins can post.
        </div>
      ) : null}

      <div className="mb-2 flex items-end gap-1.5 sm:gap-2">
        <div className="relative">
          <button
            className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-slate-700/85 text-sm font-semibold leading-none text-slate-200 hover:bg-slate-700 disabled:opacity-60 sm:h-[42px] sm:w-[42px]"
            disabled={isUploading || recordingState !== "idle" || !canSendInActiveChat}
            onClick={() => setShowAttachMenu((v) => !v)}
            type="button"
          >
            <span className="hidden min-[520px]:inline">📎</span>
            <span className="inline min-[520px]:hidden">＋</span>
          </button>
          {showAttachMenu ? (
            <div className="absolute bottom-12 left-0 z-20 w-44 rounded-xl border border-slate-700/80 bg-slate-900/95 p-1.5 shadow-2xl">
              <button
                className="block w-full rounded-lg px-3 py-2 text-left text-sm hover:bg-slate-800"
                onClick={() => mediaInputRef.current?.click()}
                type="button"
              >
                Photo or Video
              </button>
              <button
                className="block w-full rounded-lg px-3 py-2 text-left text-sm hover:bg-slate-800"
                onClick={() => fileInputRef.current?.click()}
                type="button"
              >
                File
              </button>
              <button
                className="hidden w-full rounded-lg px-3 py-2 text-left text-sm hover:bg-slate-800 max-[519px]:block"
                onClick={() => {
                  setShowAttachMenu(false);
                  setShowGifMenu(false);
                  setShowFormatMenu(false);
                  setShowStickerMenu((v) => !v);
                }}
                type="button"
              >
                Stickers
              </button>
              <button
                className="hidden w-full rounded-lg px-3 py-2 text-left text-sm hover:bg-slate-800 max-[519px]:block"
                onClick={() => {
                  setShowAttachMenu(false);
                  setShowStickerMenu(false);
                  setShowFormatMenu(false);
                  setShowGifMenu((v) => !v);
                }}
                type="button"
              >
                GIF
              </button>
              <button
                className="hidden w-full rounded-lg px-3 py-2 text-left text-sm hover:bg-slate-800 max-[519px]:block"
                onClick={() => {
                  setShowAttachMenu(false);
                  setShowStickerMenu(false);
                  setShowGifMenu(false);
                  setShowFormatMenu((v) => !v);
                }}
                type="button"
              >
                Text format
              </button>
            </div>
          ) : null}
          <input
            ref={mediaInputRef}
            className="hidden"
            type="file"
            multiple
            accept="image/*,video/*"
            disabled={isUploading || recordingState !== "idle" || !canSendInActiveChat}
            onChange={(event) => {
              const files = event.target.files ? Array.from(event.target.files) : [];
              if (files.length) {
                selectFiles(files);
              }
              event.currentTarget.value = "";
            }}
          />
          <input
            ref={fileInputRef}
            className="hidden"
            type="file"
            multiple
            disabled={isUploading || recordingState !== "idle" || !canSendInActiveChat}
            onChange={(event) => {
              const files = event.target.files ? Array.from(event.target.files) : [];
              if (files.length) {
                selectFiles(files);
              }
              event.currentTarget.value = "";
            }}
          />
        </div>

        <button
          className="hidden h-10 w-10 items-center justify-center rounded-full bg-slate-700/85 text-xs font-semibold leading-none text-slate-200 hover:bg-slate-700 disabled:opacity-60 min-[440px]:inline-flex sm:h-[42px] sm:w-[42px]"
          disabled={!canSendInActiveChat}
          onClick={() => {
            setShowGifMenu(false);
            setShowStickerMenu((v) => !v);
          }}
          type="button"
          title="Stickers"
        >
          🙂
        </button>

        <button
          className="hidden h-10 w-10 items-center justify-center rounded-full bg-slate-700/85 text-xs font-semibold leading-none text-slate-200 hover:bg-slate-700 disabled:opacity-60 min-[500px]:inline-flex sm:h-[42px] sm:w-[42px]"
          disabled={!canSendInActiveChat}
          onClick={() => {
            setShowStickerMenu(false);
            setShowGifMenu((v) => !v);
          }}
          type="button"
          title="GIF"
        >
          GIF
        </button>

        <button
          className="hidden h-10 w-10 items-center justify-center rounded-full bg-slate-700/85 text-xs font-semibold leading-none text-slate-200 hover:bg-slate-700 min-[560px]:inline-flex sm:h-[42px] sm:w-[42px]"
          onClick={() => setShowFormatMenu((v) => !v)}
          type="button"
          title="Text formatting"
          disabled={!canSendInActiveChat}
        >
          Aa
        </button>

        <textarea
          ref={textareaRef}
          className="min-h-[42px] min-w-0 max-h-40 flex-1 resize-y rounded-2xl border border-slate-700/80 bg-slate-800/80 px-3 py-2.5 text-sm outline-none placeholder:text-slate-400 focus:border-sky-500 sm:px-4"
          placeholder={canSendInActiveChat ? "Write a message..." : "Read-only channel"}
          rows={1}
          value={text}
          disabled={!canSendInActiveChat}
          onKeyDown={onComposerKeyDown}
          onChange={(event) => {
            const next = event.target.value;
            setText(next);
            if (activeChatId) {
              setDraft(activeChatId, next);
              syncTypingForText(next);
            }
          }}
          onBlur={() => {
            emitTypingStopIfActive();
          }}
        />

        {hasTextToSend ? (
          <button
            className="inline-flex h-10 w-11 shrink-0 items-center justify-center rounded-full bg-sky-500 text-sm font-semibold leading-none text-slate-950 hover:bg-sky-400 disabled:opacity-60 sm:h-[42px] sm:w-[56px]"
            disabled={recordingState !== "idle" || !activeChatId || !canSendInActiveChat}
            onClick={handleSend}
            type="button"
            title={editingMessage ? "Save edit" : "Send message"}
          >
            {editingMessage ? "✓" : "↑"}
          </button>
        ) : (
          <button
            className={`inline-flex h-10 w-11 shrink-0 items-center justify-center rounded-full text-sm font-semibold leading-none sm:h-[42px] sm:w-[56px] ${
              recordingState === "idle" ? "bg-emerald-500 text-slate-950 hover:bg-emerald-400" : "bg-slate-700 text-slate-300"
            }`}
            disabled={isUploading || !activeChatId || !canSendInActiveChat}
            onPointerDown={onMicPointerDown}
            type="button"
            title="Hold to record voice"
          >
            🎤
          </button>
        )}
      </div>

      {selectedFiles.length > 0 ? (
        <div className="fixed inset-0 z-[160] flex items-center justify-center bg-slate-950/80 p-3" onClick={cancelSelectedFile}>
          <div className="flex h-full w-full max-w-2xl flex-col rounded-2xl border border-slate-700/80 bg-slate-900/95 shadow-2xl" onClick={(event) => event.stopPropagation()}>
            <div className="flex items-center justify-between border-b border-slate-700/70 px-3 py-2">
              <div className="min-w-0">
                <p className="truncate text-sm font-semibold">Send {selectedFiles.length > 1 ? "Attachments" : (selectedType === "image" || selectedType === "video" ? "Photo" : "File")}</p>
                <p className="truncate text-xs text-slate-400">
                  {selectedFiles.length === 1
                    ? `${selectedFiles[0].name} • ${formatBytes(selectedFiles[0].size)}`
                    : `${selectedFiles.length} files • ${formatBytes(selectedFiles.reduce((acc, file) => acc + file.size, 0))}`}
                </p>
              </div>
              <button className="rounded bg-slate-700 px-2 py-1 text-xs" onClick={cancelSelectedFile} type="button">
                Close
              </button>
            </div>

            <div className="tg-scrollbar min-h-0 flex-1 overflow-auto p-3">
              {previewUrl && selectedType === "image" && selectedFiles.length === 1 ? (
                <img className="mx-auto max-h-[70vh] w-full rounded-xl object-contain" src={previewUrl} alt={selectedFiles[0].name} />
              ) : null}
              {previewUrl && selectedType === "video" && selectedFiles.length === 1 ? (
                <video className="mx-auto max-h-[70vh] w-full rounded-xl" src={previewUrl} controls muted />
              ) : null}
              {!previewUrl ? (
                <div className="rounded-lg border border-slate-700/80 bg-slate-800/60 px-3 py-2 text-xs text-slate-300">
                  {selectedFiles.slice(0, 8).map((file) => (
                    <p className="truncate" key={`${file.name}-${file.size}`}>{file.name}</p>
                  ))}
                  {selectedFiles.length > 8 ? <p className="mt-1 text-slate-400">+{selectedFiles.length - 8} more</p> : null}
                </div>
              ) : null}
            </div>

            <div className="border-t border-slate-700/70 p-3">
              <input
                className="mb-2 w-full rounded-xl border border-slate-700/80 bg-slate-800/80 px-3 py-2 text-sm outline-none placeholder:text-slate-400 focus:border-sky-500"
                maxLength={1000}
                placeholder="Add a caption..."
                value={captionDraft}
                onChange={(event) => setCaptionDraft(event.target.value)}
              />
              {isUploading ? (
                <div className="mb-2">
                  <div className="mb-1 text-xs text-slate-300">Uploading: {uploadProgress}%</div>
                  <div className="h-2 rounded bg-slate-700">
                    <div className="h-2 rounded bg-sky-500 transition-all" style={{ width: `${uploadProgress}%` }} />
                  </div>
                </div>
              ) : null}
              <div className="flex gap-2">
                <button
                  className="w-full rounded-xl bg-sky-500 px-3 py-2 font-semibold text-slate-950 disabled:opacity-50"
                  onClick={() => void sendSelectedFiles()}
                  disabled={isUploading}
                >
                  Send
                </button>
                <button className="w-full rounded-xl bg-slate-700 px-3 py-2 disabled:opacity-50" onClick={cancelSelectedFile} disabled={isUploading}>
                  Cancel
                </button>
              </div>
            </div>
          </div>
        </div>
      ) : null}

      {uploadError ? <div className="mb-2 text-sm text-red-400">{uploadError}</div> : null}
    </div>
  );
}

async function buildWaveformPoints(blob: Blob, bars = 64): Promise<number[] | null> {
  try {
    const buffer = await blob.arrayBuffer();
    const audioContext = new AudioContext();
    try {
      const audioBuffer = await audioContext.decodeAudioData(buffer.slice(0));
      const channel = audioBuffer.getChannelData(0);
      if (!channel.length || bars < 8) {
        return null;
      }
      const blockSize = Math.max(1, Math.floor(channel.length / bars));
      const points: number[] = [];
      for (let i = 0; i < bars; i += 1) {
        const start = i * blockSize;
        const end = Math.min(channel.length, start + blockSize);
        let sum = 0;
        for (let j = start; j < end; j += 1) {
          const sample = channel[j];
          sum += sample * sample;
        }
        const rms = Math.sqrt(sum / Math.max(1, end - start));
        points.push(Math.max(1, Math.min(31, Math.round(rms * 42))));
      }
      return points;
    } finally {
      await audioContext.close();
    }
  } catch {
    return null;
  }
}

function formatDuration(totalSeconds: number): string {
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;
  return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}
