"use client";

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import clsx from "clsx";
import { AlertCircle, Bold, Bookmark, CheckCheck, Clock3, Eraser, Folder as FolderIcon, Highlighter, Image as ImageIcon, Italic, Link, List, ListOrdered, Maximize2, Minimize2, Palette, Paperclip, Send, Underline, X } from "lucide-react";
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
import type { CSSProperties, ClipboardEvent, DragEvent, KeyboardEvent, ReactNode } from "react";
import { api } from "@/lib/api";
import { ContactAvatar } from "./contact-avatar";
import { createTranslator, DEFAULT_LOCALE, type TranslationKey } from "@/lib/i18n";
import type { AttachmentDraft, ComposeSeed, Contact, DriveFile, DriveStatus } from "@/lib/types";

const LARGE_FILE_LINK_THRESHOLD = 20 * 1024 * 1024;
const COMPOSE_PROXY_BODY_LIMIT_BYTES = 80 * 1024 * 1024;
const COMPOSE_PROXY_BODY_HEADROOM_BYTES = 2 * 1024 * 1024;

type RecipientFieldHandle = {
  focus: () => void;
  getValue: () => string;
};

type ComposeSize = "compact" | "expanded" | "fullscreen";

function zonedDateTimeParts(date: Date, timeZone: string) {
  const parts = new Intl.DateTimeFormat("en-CA", {
    timeZone,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    hourCycle: "h23",
  }).formatToParts(date);
  return Object.fromEntries(parts.map((part) => [part.type, part.value]));
}

function defaultScheduledDateTime(timeZone: string) {
  const date = new Date(Date.now() + 60 * 60_000);
  date.setUTCMinutes(0, 0, 0);
  const parts = zonedDateTimeParts(date, timeZone);
  return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}`;
}

function zonedDateTimeToEpoch(value: string, timeZone: string) {
  const match = value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/);
  if (!match) return Number.NaN;
  const desired = Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4]), Number(match[5]));
  let epoch = desired;
  for (let attempt = 0; attempt < 2; attempt += 1) {
    const parts = zonedDateTimeParts(new Date(epoch), timeZone);
    const rendered = Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day), Number(parts.hour), Number(parts.minute));
    epoch += desired - rendered;
  }
  return epoch;
}

function escapeHtml(value: string) {
  return value
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

function textToHtml(value: string) {
  return escapeHtml(value).replace(/\r?\n/g, "<br />");
}

function buildHtmlBody(body: string, signature: string, signatureHtml: string) {
  if (!signatureHtml.trim()) {
    return `<div>${textToHtml(body)}</div>`;
  }
  const normalizedBody = body.replace(/\r\n/g, "\n");
  const normalizedSignature = signature.replace(/\r\n/g, "\n");
  if (normalizedSignature && normalizedBody.endsWith(normalizedSignature)) {
    const messageText = normalizedBody.slice(0, -normalizedSignature.length).replace(/\n{0,2}$/, "");
    return `<div>${textToHtml(messageText)}</div><br /><br />${signatureHtml}`;
  }
  return `<div>${textToHtml(body)}</div>`;
}

function initialHtmlBody(seedBody: string, seedHtml: string | undefined, signature: string, signatureHtml: string) {
  if (seedHtml?.trim()) return seedHtml;
  const shouldAppendSignature = Boolean(signature) && !seedBody.includes(signature);
  const messageText = `${seedBody}${shouldAppendSignature && signature ? `\n\n${signature}` : ""}`;
  return buildHtmlBody(messageText, signature, signatureHtml);
}

function currentRecipientToken(value: string) {
  return value.split(",").pop()?.trim().toLowerCase() || "";
}

function parseRecipientList(value: string) {
  return value
    .split(",")
    .map((item) => item.trim())
    .filter(Boolean);
}

function recipientValue(addresses: string[]) {
  return addresses.join(", ");
}

function hasMeaningfulSubject(value: string) {
  const normalized = value.trim().toLowerCase();
  return Boolean(normalized) && normalized !== "no subject";
}

function normalizeComposeSubject(value: string) {
  return hasMeaningfulSubject(value) ? value : "";
}

function normalizeAttachmentDraft(item: AttachmentDraft) {
  return {
    filename: item.filename || "",
    content_type: item.content_type || "",
    size: Math.max(0, Number(item.size) || 0),
    kind: item.kind || "upload",
    drive_file_id: item.drive_file_id || "",
    drive_web_view_link: item.drive_web_view_link || "",
    drive_file_name: item.drive_file_name || "",
  };
}

function composeSnapshot(payload: {
  to: string;
  cc: string;
  bcc: string;
  subject: string;
  body: string;
  bodyHtml: string;
  attachments: AttachmentDraft[];
  highPriority: boolean;
  requestReadReceipt: boolean;
}) {
  return JSON.stringify({
    to: payload.to.trim(),
    cc: payload.cc.trim(),
    bcc: payload.bcc.trim(),
    subject: payload.subject.trim(),
    body: payload.body.trim(),
    bodyHtml: payload.bodyHtml.trim(),
    attachments: payload.attachments.map(normalizeAttachmentDraft),
    highPriority: payload.highPriority,
    requestReadReceipt: payload.requestReadReceipt,
  });
}

function fileToBase64(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(String(reader.result).split(",")[1] || "");
    reader.onerror = () => reject(reader.error);
    reader.readAsDataURL(file);
  });
}

function fileToDataUrl(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(String(reader.result || ""));
    reader.onerror = () => reject(reader.error);
    reader.readAsDataURL(file);
  });
}

function fileExtension(value: string) {
  const clean = value.trim().toLowerCase().split("?")[0].split("#")[0];
  const fileName = clean.split("/").pop() || clean;
  const match = fileName.match(/\.([a-z0-9]{1,8})$/i);
  return match ? match[1].toLowerCase() : "";
}

function formatDriveMimeLabel(t: (key: TranslationKey, values?: Record<string, string | number>) => string, mimeType: string, filename = "") {
  const value = mimeType.trim().toLowerCase();
  const ext = fileExtension(filename);
  if (!value) return "";
  if (value === "application/pdf" || ext === "pdf") return "PDF";
  if (value === "application/vnd.google-apps.folder") return t("compose.fileTypeFolder");
  if (value === "application/vnd.google-apps.document") return t("compose.fileTypeGoogleDoc");
  if (value === "application/vnd.google-apps.spreadsheet") return t("compose.fileTypeGoogleSheet");
  if (value === "application/vnd.google-apps.presentation") return t("compose.fileTypeGoogleSlides");
  if (value === "application/vnd.google-apps.drawing") return t("compose.fileTypeDrawing");
  if (value.startsWith("image/") || ["jpg", "jpeg", "png", "gif", "webp", "bmp", "heic", "heif", "avif", "tif", "tiff"].includes(ext)) return t("compose.fileTypeImage");
  if (value.startsWith("video/")) return t("compose.fileTypeVideo");
  if (value.startsWith("audio/")) return t("compose.fileTypeAudio");
  if (value === "application/zip" || ["zip", "rar", "7z", "tar", "gz", "tgz"].includes(ext)) return "ZIP";
  if (value === "text/plain") return t("compose.fileTypeText");
  if (value === "application/octet-stream") return ext ? ext.toUpperCase() : t("compose.file");
  if (value.startsWith("application/vnd.google-apps.")) return t("compose.fileTypeGoogleFile");
  return t("compose.file");
}

function formatBytes(value: number) {
  const bytes = Math.max(0, Number(value) || 0);
  if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
  if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
  if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
  return `${bytes} B`;
}

function isMailServerUploadAttachment(item: AttachmentDraft) {
  return (item.kind || "upload") !== "drive_link";
}

function encodedAttachmentSize(bytes: number) {
  const size = Math.max(0, Number(bytes) || 0);
  if (!size) return 0;
  return Math.ceil(size / 3) * 4;
}

export function ComposeDialog({
  open,
  onClose,
  replyTo,
  focusTarget = "body",
  signature = "",
  signatureHtml = "",
  contacts = [],
  driveStatus,
  driveServiceLabel = "Drive",
  allowDriveLink = true,
  canBrowseStorageAttachments = false,
  accent = "#0891b2",
  timeZone = "Asia/Bangkok",
  t = createTranslator(DEFAULT_LOCALE),
}: {
  open: boolean;
  onClose: () => void;
  replyTo?: ComposeSeed;
  focusTarget?: "to" | "body";
  signature?: string;
  signatureHtml?: string;
  contacts?: Contact[];
  driveStatus?: DriveStatus | null;
  driveServiceLabel?: string;
  allowDriveLink?: boolean;
  canBrowseStorageAttachments?: boolean;
  accent?: string;
  timeZone?: string;
  t?: (key: TranslationKey, values?: Record<string, string | number>) => string;
}) {
  const queryClient = useQueryClient();
  const [attachments, setAttachments] = useState<AttachmentDraft[]>([]);
  const [dragging, setDragging] = useState(false);
  const [draftId, setDraftId] = useState("");
  const [to, setTo] = useState("");
  const [cc, setCc] = useState("");
  const [bcc, setBcc] = useState("");
  const [showCc, setShowCc] = useState(false);
  const [showBcc, setShowBcc] = useState(false);
  const [subject, setSubject] = useState("");
  const [body, setBody] = useState("");
  const [bodyHtml, setBodyHtml] = useState("");
  const [editorDragging, setEditorDragging] = useState(false);
  const [confirmNoSubject, setConfirmNoSubject] = useState(false);
  const [confirmUnsavedClose, setConfirmUnsavedClose] = useState(false);
  const [drivePickerOpen, setDrivePickerOpen] = useState(false);
  const [drivePickerFolderId, setDrivePickerFolderId] = useState("");
  const [driveSearch, setDriveSearch] = useState("");
  const [attachmentError, setAttachmentError] = useState("");
  const [scheduleOpen, setScheduleOpen] = useState(false);
  const [scheduledDateTime, setScheduledDateTime] = useState("");
  const [scheduleError, setScheduleError] = useState("");
  const [pendingSendAtMs, setPendingSendAtMs] = useState<number | undefined>();
  const [highPriority, setHighPriority] = useState(false);
  const [requestReadReceipt, setRequestReadReceipt] = useState(false);
  const [composeSize, setComposeSize] = useState<ComposeSize>("expanded");
  const [minimized, setMinimized] = useState(false);
  const editorRef = useRef<HTMLDivElement | null>(null);
  const attachmentInputRef = useRef<HTMLInputElement | null>(null);
  const imageInputRef = useRef<HTMLInputElement | null>(null);
  const inlineImageInputId = "compose-inline-image-picker";
  const selectionRef = useRef<Range | null>(null);
  const toFieldRef = useRef<RecipientFieldHandle | null>(null);
  const ccFieldRef = useRef<RecipientFieldHandle | null>(null);
  const bccFieldRef = useRef<RecipientFieldHandle | null>(null);
  const initialSnapshotRef = useRef("");
  const driveFiles = useQuery({
    queryKey: ["drive-contents", drivePickerFolderId, driveSearch],
    queryFn: () => api.driveContents(drivePickerFolderId, driveSearch, 40),
    enabled: open && drivePickerOpen && Boolean(driveStatus?.connected),
    staleTime: 5 * 60_000,
    refetchOnWindowFocus: false,
    retry: false,
  });
  const driveFolders = useQuery({
    queryKey: ["drive-folders", drivePickerFolderId],
    queryFn: () => api.driveFolders("", 500),
    enabled: open && drivePickerOpen && Boolean(driveStatus?.connected),
    staleTime: 5 * 60_000,
    refetchOnWindowFocus: false,
    retry: false,
  });
  const driveCurrentFolder = useQuery({
    queryKey: ["drive-folder", drivePickerFolderId],
    queryFn: () => api.driveFile(drivePickerFolderId),
    enabled: open && drivePickerOpen && Boolean(driveStatus?.connected && drivePickerFolderId),
    staleTime: 5 * 60_000,
    refetchOnWindowFocus: false,
    retry: false,
  });
  const mailLimits = useQuery({
    queryKey: ["mail-limits"],
    queryFn: () => api.mailLimits(),
    enabled: open,
    staleTime: 5 * 60_000,
    refetchOnWindowFocus: false,
    retry: false,
  });
  const driveRootLabel = driveServiceLabel === "Briefcase" ? "Briefcase" : driveServiceLabel;
  const driveFolderMap = useMemo(() => new Map((driveFolders.data?.folders ?? []).map((file) => [file.id, file])), [driveFolders.data?.folders]);
  const drivePickerCurrentFolder = driveCurrentFolder.data?.file;
  const drivePickerParentId = drivePickerCurrentFolder?.parent_ids.find((parentId) => parentId && parentId !== drivePickerFolderId) ?? "";
  const drivePickerParentDestination = drivePickerParentId === "1" || drivePickerParentId === "root" ? "" : drivePickerParentId;
  const drivePickerShowParentRow = Boolean(
    drivePickerFolderId &&
    drivePickerParentId &&
    !(driveServiceLabel === "Briefcase" && drivePickerCurrentFolder?.name === driveServiceLabel)
  );
  const drivePickerBreadcrumbs = useMemo(() => {
    if (!drivePickerFolderId) return [];
    const breadcrumbs: DriveFile[] = [];
    const seen = new Set<string>();
    let current: DriveFile | undefined = drivePickerCurrentFolder || driveFolderMap.get(drivePickerFolderId);
    while (current && current.id && !seen.has(current.id)) {
      breadcrumbs.unshift(current);
      seen.add(current.id);
      const nextParentId = current.parent_ids.find((parentId) => parentId && driveFolderMap.has(parentId));
      if (!nextParentId) break;
      current = driveFolderMap.get(nextParentId);
    }
    return breadcrumbs;
  }, [drivePickerCurrentFolder, driveFolderMap, drivePickerFolderId]);
  const drivePickerDisplayBreadcrumbs = useMemo(
    () => drivePickerBreadcrumbs.filter((crumb) => crumb.name !== driveServiceLabel),
    [drivePickerBreadcrumbs, driveServiceLabel]
  );

  useEffect(() => {
    if (!open) return;
    setDraftId(replyTo?.draft_id || "");
    setTo(replyTo?.to || "");
    setCc(replyTo?.cc || "");
    setBcc(replyTo?.bcc || "");
    setShowCc(Boolean((replyTo?.cc || "").trim()));
    setShowBcc(Boolean((replyTo?.bcc || "").trim()));
    const seedSubject = normalizeComposeSubject(replyTo?.subject || "");
    setSubject(seedSubject);
    const seedBody = replyTo?.body || "";
    const seedHtml = !replyTo?.draft_id ? initialHtmlBody(seedBody, replyTo?.body_html, signature, signatureHtml) : (replyTo?.body_html || buildHtmlBody(seedBody, "", ""));
    setBodyHtml(seedHtml);
    setBody(htmlToText(seedHtml));
    setAttachments(replyTo?.attachments || []);
    setHighPriority(Boolean(replyTo?.high_priority));
    setRequestReadReceipt(Boolean(replyTo?.request_read_receipt));
    setAttachmentError("");
    setDrivePickerFolderId("");
    setConfirmNoSubject(false);
    setConfirmUnsavedClose(false);
    setScheduleOpen(false);
    setScheduleError("");
    setPendingSendAtMs(undefined);
    setComposeSize("expanded");
    setMinimized(false);
    initialSnapshotRef.current = composeSnapshot({
      to: replyTo?.to || "",
      cc: replyTo?.cc || "",
      bcc: replyTo?.bcc || "",
      subject: seedSubject,
      body: htmlToText(seedHtml),
      bodyHtml: seedHtml,
      attachments: replyTo?.attachments || [],
      highPriority: Boolean(replyTo?.high_priority),
      requestReadReceipt: Boolean(replyTo?.request_read_receipt),
    });
  }, [open, replyTo, signature, signatureHtml]);

  useEffect(() => {
    if (!open) return;
    window.requestAnimationFrame(() => {
      if (focusTarget === "to") {
        toFieldRef.current?.focus();
        return;
      }
      editorRef.current?.focus();
    });
  }, [focusTarget, open]);

  useEffect(() => {
    if (!open || !editorRef.current) return;
    if (editorRef.current.innerHTML !== bodyHtml) {
      editorRef.current.innerHTML = bodyHtml;
    }
  }, [open, bodyHtml]);

  function resetCompose() {
    setDraftId("");
    setTo("");
    setCc("");
    setBcc("");
    setShowCc(false);
    setShowBcc(false);
    setSubject("");
    setBody("");
    setBodyHtml("");
    setAttachments([]);
    setAttachmentError("");
    setDrivePickerOpen(false);
    setDrivePickerFolderId("");
    setScheduleOpen(false);
    setScheduleError("");
    setPendingSendAtMs(undefined);
  }

  const currentSnapshot = useMemo(
    () =>
      composeSnapshot({
        to,
        cc,
        bcc,
        subject,
        body,
        bodyHtml,
        attachments,
        highPriority,
        requestReadReceipt,
      }),
    [attachments, bcc, body, bodyHtml, cc, highPriority, requestReadReceipt, subject, to]
  );
  const isDirty = currentSnapshot !== initialSnapshotRef.current;

  function requestClose() {
    if (confirmUnsavedClose) {
      setConfirmUnsavedClose(false);
      return;
    }
    if (isDirty) {
      setConfirmNoSubject(false);
      setConfirmUnsavedClose(true);
      return;
    }
    onClose();
  }

  const send = useMutation({
    mutationFn: api.send,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["contacts"] });
      queryClient.invalidateQueries({ queryKey: ["messages"] });
      queryClient.invalidateQueries({ queryKey: ["message"] });
      queryClient.invalidateQueries({ queryKey: ["message-thread-detail"] });
      queryClient.invalidateQueries({ queryKey: ["folders"] });
      resetCompose();
      onClose();
    }
  });

  const saveDraft = useMutation({
    mutationFn: api.saveDraft,
    onSuccess: (data, variables) => {
      const draft = variables as {
        to?: string;
        cc?: string;
        bcc?: string;
        subject?: string;
        body?: string;
        body_html?: string;
        attachments?: AttachmentDraft[];
        draft_id?: string;
        origid?: string;
        rt?: string;
      };
      const nextDraftId = data.draft_id || draft.draft_id || draftId;
      setDraftId(nextDraftId);
      queryClient.invalidateQueries({ queryKey: ["messages"] });
      queryClient.invalidateQueries({ queryKey: ["message"] });
      queryClient.invalidateQueries({ queryKey: ["folders"] });
      queryClient.invalidateQueries({ queryKey: ["message-thread-detail"] });
      resetCompose();
      onClose();
    }
  });

  async function addFiles(files: FileList | null) {
    if (!files) return;
    const fileList = Array.from(files);
    const directUploadSize = fileList
      .filter((file) => !(driveStatus?.connected && file.size >= LARGE_FILE_LINK_THRESHOLD))
      .reduce((sum, file) => sum + encodedAttachmentSize(file.size), 0);
    if (!validateAttachmentLimit(directUploadSize)) {
      return;
    }
    try {
      const incoming = await Promise.all(fileList.map(async (file) => {
        if (allowDriveLink && driveStatus?.connected && file.size >= LARGE_FILE_LINK_THRESHOLD) {
          const uploaded = await api.driveUpload({
            filename: file.name,
            content_type: file.type || "application/octet-stream",
            content_base64: await fileToBase64(file),
          });
          const shared = await api.driveShareLink(uploaded.file.id);
          return {
            filename: shared.file.name || file.name,
            content_type: shared.file.mime_type || file.type || "application/octet-stream",
            size: file.size,
            kind: "drive_link" as const,
            drive_file_id: shared.file.id,
            drive_web_view_link: shared.file.web_view_link,
            drive_file_name: shared.file.name || file.name,
          };
        }
        return {
          filename: file.name,
          content_type: file.type || "application/octet-stream",
          size: file.size,
          content_base64: await fileToBase64(file),
          kind: "upload" as const,
        };
      }));
      setAttachments((current) => [...current, ...incoming]);
      setAttachmentError("");
    } catch (error) {
      setAttachmentError(error instanceof Error ? error.message : t("compose.unableAddAttachment"));
    }
  }

  function removeAttachment(index: number) {
    setAttachments((current) => current.filter((_, currentIndex) => currentIndex !== index));
    setAttachmentError("");
  }

  function openAttachmentPicker() {
    attachmentInputRef.current?.click();
  }

  function openImagePicker() {
    const input = imageInputRef.current;
    if (!input) return;
    const picker = input as HTMLInputElement & { showPicker?: () => void };
    if (typeof picker.showPicker === "function") {
      picker.showPicker();
      return;
    }
    picker.click();
  }

  function attachDriveFile(file: DriveFile, mode: "drive_file" | "drive_link") {
    const item = {
      filename: file.name,
      content_type: file.mime_type,
      size: file.size,
      kind: mode,
      drive_file_id: file.id,
      drive_web_view_link: file.web_view_link,
      drive_file_name: file.name,
    } as AttachmentDraft;
    const next = [...attachments, item];
    if (!validateAttachmentLimit(0, next)) {
      return;
    }
    setAttachments(next);
    setAttachmentError("");
    setDrivePickerOpen(false);
  }

  function uploadAttachmentSize(extraBytes = 0, candidates = attachments) {
    return candidates
      .filter(isMailServerUploadAttachment)
      .reduce((sum, item) => sum + Math.max(0, Number(item.size) || 0), Math.max(0, extraBytes));
  }

  function activeAttachmentLimit() {
    const limits = mailLimits.data?.limits;
    const uploadLimit = Math.max(0, Number(limits?.max_upload_bytes) || 0);
    const proxyLimit = COMPOSE_PROXY_BODY_LIMIT_BYTES - COMPOSE_PROXY_BODY_HEADROOM_BYTES;
    return uploadLimit ? Math.min(uploadLimit, proxyLimit) : proxyLimit;
  }

  function validateAttachmentLimit(extraBytes = 0, candidates = attachments) {
    const limit = activeAttachmentLimit();
    if (!limit) return true;
    const total = uploadAttachmentSize(extraBytes, candidates);
    if (total <= limit) return true;
    setAttachmentError(t("compose.attachmentTooLarge", { limit: formatBytes(limit), total: formatBytes(total) }));
    return false;
  }

  function saveSelection() {
    const editor = editorRef.current;
    const selection = window.getSelection();
    if (!editor || !selection?.rangeCount) return;
    const range = selection.getRangeAt(0);
    if (editor.contains(range.commonAncestorContainer)) {
      selectionRef.current = range.cloneRange();
    }
  }

  function restoreSelection() {
    const editor = editorRef.current;
    const range = selectionRef.current;
    editor?.focus();
    if (!range) return;
    const selection = window.getSelection();
    selection?.removeAllRanges();
    selection?.addRange(range);
  }

  function syncEditor() {
    const editor = editorRef.current;
    if (!editor) return;
    setBodyHtml(editor.innerHTML);
    setBody(editorPlainText(editor));
    saveSelection();
  }

  function format(command: string, value?: string) {
    restoreSelection();
    if (command === "insertUnorderedList" && !editorRef.current?.innerText.trim()) {
      document.execCommand("insertHTML", false, "<ul><li><br></li></ul>");
      syncEditor();
      return;
    }
    if (command === "insertOrderedList" && !editorRef.current?.innerText.trim()) {
      document.execCommand("insertHTML", false, "<ol><li><br></li></ol>");
      syncEditor();
      return;
    }
    document.execCommand(command, false, value);
    syncEditor();
  }

  function addLink() {
    const href = window.prompt(t("compose.linkUrl"));
    if (!href) return;
    format("createLink", href);
  }

  async function insertInlineImages(files: File[]) {
    const imageFiles = files.filter((file) => file.type.startsWith("image/"));
    if (!imageFiles.length) return false;
    restoreSelection();
    for (const file of imageFiles) {
      const dataUrl = await fileToDataUrl(file);
      document.execCommand(
        "insertHTML",
        false,
        `<p><img src="${dataUrl}" alt="" title="${escapeHtml(file.name)}" data-filename="${escapeHtml(file.name)}" style="max-width:100%;height:auto;" /></p>`
      );
    }
    syncEditor();
    return true;
  }

  async function handleEditorPaste(event: ClipboardEvent<HTMLDivElement>) {
    const files = Array.from(event.clipboardData.files);
    if (!files.some((file) => file.type.startsWith("image/"))) return;
    event.preventDefault();
    await insertInlineImages(files);
  }

  async function handleEditorDrop(event: DragEvent<HTMLDivElement>) {
    const files = Array.from(event.dataTransfer.files);
    if (!files.length) return;
    event.preventDefault();
    setEditorDragging(false);
    const inserted = await insertInlineImages(files);
    if (!inserted) {
      await addFiles(event.dataTransfer.files);
    }
  }

  function sendCurrentMessage(sendAtMs?: number) {
    syncEditor();
    if (!validateAttachmentLimit()) return;
    const html = editorRef.current?.innerHTML || bodyHtml;
    const text = editorPlainText(editorRef.current) || body;
    const nextTo = toFieldRef.current?.getValue() ?? to;
    const nextCc = ccFieldRef.current?.getValue() ?? cc;
    const nextBcc = bccFieldRef.current?.getValue() ?? bcc;
    send.mutate({
      to: nextTo,
      cc: nextCc,
      bcc: nextBcc,
      subject,
      body: text,
      body_html: html,
      attachments,
      draft_id: draftId,
      send_at_ms: sendAtMs,
      high_priority: highPriority,
      request_read_receipt: requestReadReceipt,
    });
  }

  function submit(sendAtMs?: number) {
    if (!hasMeaningfulSubject(subject)) {
      setPendingSendAtMs(sendAtMs);
      setConfirmNoSubject(true);
      return;
    }
    sendCurrentMessage(sendAtMs);
  }

  function openSchedule() {
    setScheduledDateTime(defaultScheduledDateTime(timeZone));
    setScheduleError("");
    setScheduleOpen(true);
  }

  function scheduleMessage() {
    const sendAtMs = zonedDateTimeToEpoch(scheduledDateTime, timeZone);
    if (!Number.isFinite(sendAtMs) || sendAtMs < Date.now() + 60_000) {
      setScheduleError(t("compose.scheduleFutureError"));
      return;
    }
    setScheduleOpen(false);
    setScheduleError("");
    submit(sendAtMs);
  }

  function saveCurrentDraft() {
    syncEditor();
    if (!validateAttachmentLimit()) return;
    const html = editorRef.current?.innerHTML || bodyHtml;
    const text = editorPlainText(editorRef.current) || body;
    const nextTo = toFieldRef.current?.getValue() ?? to;
    const nextCc = ccFieldRef.current?.getValue() ?? cc;
    const nextBcc = bccFieldRef.current?.getValue() ?? bcc;
    saveDraft.mutate({
      to: nextTo,
      cc: nextCc,
      bcc: nextBcc,
      subject,
      body: text,
      body_html: html,
      attachments,
      draft_id: draftId,
      origid: replyTo?.origid || "",
      rt: replyTo?.rt || "",
      high_priority: highPriority,
      request_read_receipt: requestReadReceipt,
    });
    return true;
  }

  if (!open) return null;

  const composeFrameClass = clsx(
    "pointer-events-auto grid h-[100dvh] w-full max-w-none grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden rounded-none border border-slate-200/80 bg-white shadow-soft sm:rounded-xl",
    composeSize === "compact" && "sm:h-[70vh] sm:max-h-[760px] sm:w-[min(42rem,calc(100vw-2rem))] sm:max-w-none",
    composeSize === "expanded" && "sm:h-[88vh] sm:max-h-[920px] sm:w-[clamp(36rem,60vw,68rem)] sm:max-w-[calc(100vw-2rem)]",
    composeSize === "fullscreen" && "sm:h-[calc(100dvh-2rem)] sm:max-h-none sm:w-[calc(100vw-2rem)] sm:max-w-none"
  );
  const composerBodyClass = clsx(
    "grid min-h-0 gap-3 px-4 pb-0 pt-4 sm:px-5 sm:pb-0 sm:pt-5",
    composeSize === "compact" ? "overflow-y-auto" : "grid-rows-[auto_auto_minmax(0,1fr)_auto] overflow-hidden"
  );
  const editorClass = clsx(
    "overflow-auto p-3 pb-20 text-sm leading-6 outline-none empty:before:text-slate-400 empty:before:content-[attr(data-placeholder)] [&_a]:text-[var(--accent)] [&_a]:underline [&_img]:my-2 [&_img]:max-h-[360px] [&_img]:max-w-full [&_img]:object-contain [&_li]:ml-5 [&_li]:list-item [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:list-disc [&_ul]:pl-5",
    composeSize === "compact" ? "min-h-56" : "min-h-0"
  );
  const composeTitle = replyTo?.draft_id ? t("compose.editDraft") : replyTo?.origid ? t("compose.replyDraft") : t("compose.newMessage");

  if (minimized) {
    return (
      <div className="pointer-events-none fixed inset-0 z-50 flex items-end justify-end p-3 sm:p-4" style={{ "--accent": accent } as CSSProperties}>
        <button
          type="button"
          onClick={() => setMinimized(false)}
          className="pointer-events-auto flex min-h-12 w-[min(calc(100vw-1.5rem),26rem)] items-center justify-between gap-3 rounded-2xl border border-slate-200 bg-white px-4 py-3 text-left shadow-[0_20px_70px_-28px_rgba(15,23,42,0.55)] transition hover:border-[var(--accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2"
          aria-label="Restore compose"
          title="Restore compose"
        >
          <span className="min-w-0">
            <span className="block text-xs font-black uppercase tracking-[0.12em] text-[var(--accent)]">{composeTitle}</span>
            <span className="block truncate text-sm font-semibold text-slate-700">{subject.trim() || to.trim() || t("compose.writeMessage")}</span>
          </span>
          <Maximize2 size={17} className="shrink-0 text-slate-500" />
        </button>
      </div>
    );
  }

  return (
    <div className="pointer-events-auto fixed inset-0 z-50 grid place-items-end bg-slate-950/25 p-0 backdrop-blur-sm sm:pointer-events-none sm:inset-auto sm:bottom-4 sm:right-4 sm:block sm:bg-transparent sm:backdrop-blur-0">
      <form
        onSubmit={(event) => {
          event.preventDefault();
          submit();
        }}
        className={composeFrameClass}
        style={{ "--accent": accent } as CSSProperties}
      >
        <header className="flex items-center justify-between border-b border-slate-200 px-4 py-3 sm:px-5 sm:py-4">
          <strong>{composeTitle}</strong>
          <div className="flex items-center gap-1">
            <button
              type="button"
              onClick={() => {
                syncEditor();
                setMinimized(true);
              }}
              className="hidden h-9 items-center gap-1.5 rounded-lg border border-transparent px-3 text-xs font-bold text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 sm:inline-flex"
              aria-label="Minimize compose"
              title="Minimize compose"
            >
              <Minimize2 size={15} />
              <span>Minimize</span>
            </button>
            <button
              type="button"
              onClick={() => setComposeSize((current) => (current === "fullscreen" ? "expanded" : "fullscreen"))}
              className="icon-button hidden h-9 w-9 border-transparent sm:inline-flex"
              aria-label={composeSize === "fullscreen" ? "Restore compose" : "Expand compose"}
              title={composeSize === "fullscreen" ? "Restore compose" : "Expand compose"}
            >
              {composeSize === "fullscreen" ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
            </button>
            <button type="button" onClick={requestClose} className="icon-button h-9 w-9 border-transparent" aria-label={t("compose.close")}>
              <X size={18} />
            </button>
          </div>
        </header>

        <div className={composerBodyClass}>
          <div className="grid gap-2">
            <div className="flex items-end gap-2">
              <div className="min-w-0 flex-1">
                <RecipientField ref={toFieldRef} label="To" displayLabel={t("compose.toDisplay")} removeLabel={(address) => t("compose.removeRecipient", { address })} value={to} onChange={setTo} contacts={contacts} required />
              </div>
              <div className="mb-1 flex shrink-0 items-center gap-1">
                <button
                  type="button"
                  onClick={() => setShowCc((current) => !current)}
                  className={clsx("h-8 rounded-md px-2 text-xs font-black transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2", showCc ? "bg-[color-mix(in_srgb,var(--accent)_10%,white)] text-[var(--accent)]" : "text-slate-500 hover:bg-slate-100 hover:text-slate-700")}
                  aria-expanded={showCc}
                >
                  Cc
                </button>
                <button
                  type="button"
                  onClick={() => setShowBcc((current) => !current)}
                  className={clsx("h-8 rounded-md px-2 text-xs font-black transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2", showBcc ? "bg-[color-mix(in_srgb,var(--accent)_10%,white)] text-[var(--accent)]" : "text-slate-500 hover:bg-slate-100 hover:text-slate-700")}
                  aria-label={showBcc ? t("compose.hideBcc") : t("compose.showBcc")}
                  aria-expanded={showBcc}
                >
                  Bcc
                </button>
              </div>
            </div>
            {showCc ? <RecipientField ref={ccFieldRef} label="Cc" displayLabel={t("compose.ccDisplay")} removeLabel={(address) => t("compose.removeRecipient", { address })} value={cc} onChange={setCc} contacts={contacts} /> : null}
            {showBcc ? <RecipientField ref={bccFieldRef} label="Bcc" displayLabel={t("compose.bccDisplay")} removeLabel={(address) => t("compose.removeRecipient", { address })} value={bcc} onChange={setBcc} contacts={contacts} /> : null}
          </div>
          <div className="flex items-center gap-2">
            <input name="subject" value={subject} onChange={(event) => setSubject(event.target.value)} className="field h-11 min-w-0 flex-1" placeholder={t("compose.subject")} />
            <div className="flex shrink-0 items-center gap-1">
              <button
                type="button"
                onClick={() => setHighPriority((value) => !value)}
                className={clsx(
                  "inline-flex h-8 items-center gap-1.5 rounded-md px-2 text-xs font-black transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2",
                  highPriority ? "bg-red-50 text-red-700" : "text-slate-500 hover:bg-slate-100 hover:text-slate-700"
                )}
                aria-label={t("compose.highPriority")}
                aria-pressed={highPriority}
                title={t("compose.highPriority")}
              >
                <AlertCircle size={15} />
                <span className="hidden lg:inline">{t("compose.highPriority")}</span>
              </button>
              <button
                type="button"
                onClick={() => setRequestReadReceipt((value) => !value)}
                className={clsx(
                  "inline-flex h-8 items-center gap-1.5 rounded-md px-2 text-xs font-black transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2",
                  requestReadReceipt ? "bg-emerald-50 text-emerald-700" : "text-slate-500 hover:bg-slate-100 hover:text-slate-700"
                )}
                aria-label={t("compose.requestReadReceipt")}
                aria-pressed={requestReadReceipt}
                title={t("compose.requestReadReceipt")}
              >
                <CheckCheck size={15} />
                <span className="hidden lg:inline">{t("compose.requestReadReceipt")}</span>
              </button>
            </div>
          </div>
          <div className={`grid min-h-0 grid-rows-[auto_minmax(0,1fr)] overflow-hidden rounded-xl border bg-white transition ${editorDragging ? "border-[var(--accent)] ring-4 ring-[color-mix(in_srgb,var(--accent)_14%,white)]" : "border-slate-200"}`}>
            <div className="flex flex-wrap items-center gap-1 border-b border-slate-200 bg-slate-50 px-2 py-2">
              <FormatButton label={t("compose.bold")} onClick={() => format("bold")} icon={<Bold size={16} />} />
              <FormatButton label={t("compose.italic")} onClick={() => format("italic")} icon={<Italic size={16} />} />
              <FormatButton label={t("compose.underline")} onClick={() => format("underline")} icon={<Underline size={16} />} />
              <FormatButton label={t("compose.bulletedList")} onClick={() => format("insertUnorderedList")} icon={<List size={16} />} />
              <FormatButton label="Numbered list" onClick={() => format("insertOrderedList")} icon={<ListOrdered size={16} />} />
              <FormatButton label={t("compose.insertLink")} onClick={addLink} icon={<Link size={16} />} />
              <ColorButton label={t("compose.textColor")} icon={<Palette size={16} />} onChange={(color) => format("foreColor", color)} />
              <ColorButton label="Highlight color" icon={<Highlighter size={16} />} onChange={(color) => format("hiliteColor", color)} />
              <FormatButton label="Clear formatting" onClick={() => format("removeFormat")} icon={<Eraser size={16} />} />
              <FormatButton
                label={t("compose.pasteImages")}
                onClick={openImagePicker}
                icon={<ImageIcon size={16} />}
                dataAttachmentAction
              />
            </div>
            <div
              ref={editorRef}
              contentEditable
              onInput={syncEditor}
              onMouseUp={saveSelection}
              onKeyUp={saveSelection}
              onPaste={(event) => void handleEditorPaste(event)}
              onDragOver={(event) => {
                if (!Array.from(event.dataTransfer.types).includes("Files")) return;
                event.preventDefault();
                setEditorDragging(true);
              }}
              onDragLeave={() => setEditorDragging(false)}
              onDrop={(event) => void handleEditorDrop(event)}
              className={editorClass}
              data-placeholder={t("compose.writeMessage")}
              role="textbox"
              aria-label={t("compose.messageBody")}
              aria-multiline="true"
            />
          </div>

          <div className="grid gap-2">
            <div className="flex flex-wrap items-stretch gap-2">
              <div
                onDragOver={(event) => {
                  event.preventDefault();
                  setDragging(true);
                }}
                onDragLeave={() => setDragging(false)}
                onDrop={(event) => {
                  event.preventDefault();
                  setDragging(false);
                  addFiles(event.dataTransfer.files);
                }}
                onClick={(event) => {
                  if ((event.target as HTMLElement).closest("[data-attachment-action]")) return;
                  openAttachmentPicker();
                }}
                onKeyDown={(event) => {
                  if (event.key !== "Enter" && event.key !== " ") return;
                  event.preventDefault();
                  openAttachmentPicker();
                }}
                className={`relative flex min-h-[132px] flex-1 cursor-pointer rounded-lg border border-dashed px-4 py-3 text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 ${dragging ? "border-[var(--accent)] bg-[color-mix(in_srgb,var(--accent)_10%,white)] text-[var(--accent)]" : "border-slate-200 text-slate-500 hover:bg-slate-50"}`}
                role="button"
                tabIndex={0}
                aria-label={t("compose.dragAttachments")}
              >
                <input
                  ref={attachmentInputRef}
                  type="file"
                  multiple
                  className="sr-only"
                  onChange={async (event) => {
                    await addFiles(event.target.files);
                    event.currentTarget.value = "";
                  }}
                />
                <input
                  ref={imageInputRef}
                  id={inlineImageInputId}
                  type="file"
                  accept="image/*"
                  multiple
                  className="sr-only"
                  onChange={async (event) => {
                    const files = Array.from(event.currentTarget.files || []);
                    event.currentTarget.value = "";
                    if (!files.length) return;
                    await insertInlineImages(files);
                  }}
                />
                {canBrowseStorageAttachments && driveStatus?.connected ? (
                  <button
                    type="button"
                    data-attachment-action
                    onClick={() => setDrivePickerOpen(true)}
                    className="theme-button-soft absolute right-3 top-3 h-8 px-3 text-xs"
                    aria-label={t("compose.browseDrive", { drive: driveServiceLabel })}
                  >
                    <FolderIcon size={15} />
                    {t("compose.browseDrive", { drive: driveServiceLabel })}
                  </button>
                ) : null}
                <div className="flex w-full flex-col gap-3">
                  <div className={clsx("flex min-h-8 items-center justify-center gap-2 text-center", canBrowseStorageAttachments && driveStatus?.connected && "pr-36 sm:pr-44")}>
                    <Paperclip size={17} />
                    <span>{t("compose.dragAttachments")}</span>
                  </div>
                  {attachments.length ? (
                    <div className="flex max-h-24 flex-wrap content-start gap-2 overflow-y-auto pr-1">
                      {attachments.map((file, index) => (
                        <div key={`${file.filename}-${file.size}-${index}`} className="inline-flex max-w-full items-center gap-2 rounded-full bg-white px-3 py-1 text-xs font-bold text-slate-600 shadow-sm ring-1 ring-slate-200">
                          <span className={clsx("shrink-0 rounded-full px-2 py-0.5 text-[10px] uppercase tracking-[0.12em]", file.kind === "drive_link" ? "bg-emerald-100 text-emerald-800" : file.kind === "drive_file" ? "bg-[color-mix(in_srgb,var(--accent)_12%,white)] text-[var(--accent)]" : "bg-slate-200 text-slate-700")}>
                            {file.kind === "drive_link" ? t("compose.link") : file.kind === "drive_file" ? driveServiceLabel : t("compose.file")}
                          </span>
                          <span className="min-w-0 truncate">
                            {file.filename} ({Math.max(1, Math.round(file.size / 1024))} KB)
                          </span>
                          <button
                            type="button"
                            data-attachment-action
                            onClick={() => removeAttachment(index)}
                            className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-200 hover:text-slate-700"
                            aria-label={t("compose.removeAttachment", { filename: file.filename })}
                            title={t("compose.removeAttachment", { filename: file.filename })}
                          >
                            <X size={12} />
                          </button>
                        </div>
                      ))}
                    </div>
                  ) : null}
                </div>
              </div>
            </div>
            {attachmentError ? <p className="text-sm font-semibold text-red-600">{attachmentError}</p> : null}
          </div>
        </div>

        <footer className="flex shrink-0 flex-wrap items-center justify-between gap-3 border-t border-slate-200 bg-white px-4 py-3 sm:px-5 sm:py-4">
          <div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
          <p className={clsx("min-w-0 flex-1 truncate text-sm font-semibold", send.error?.message || saveDraft.error?.message ? "text-red-600" : "text-slate-500")}> 
            {send.error?.message || saveDraft.error?.message || (activeAttachmentLimit() ? t("compose.attachmentLimit", { limit: formatBytes(activeAttachmentLimit()) }) : "")}
          </p>
          </div>
          <div className="flex items-center gap-2">
              <button
                type="button"
                onClick={() => {
                  saveCurrentDraft();
                }}
                className="theme-button-soft h-10 px-4 disabled:opacity-60"
                disabled={send.isPending || saveDraft.isPending}
              >
                <Bookmark size={17} />
                {saveDraft.isPending ? t("compose.saving") : draftId ? t("compose.updateDraft") : t("compose.saveDraft")}
              </button>
              <div className="flex overflow-hidden rounded-lg shadow-sm">
              <button
                type="button"
                onClick={() => submit()}
                className="theme-button h-10 rounded-r-none px-4 disabled:opacity-60"
                disabled={send.isPending || saveDraft.isPending}
              >
                <Send size={17} />
                {send.isPending ? t("compose.sending") : t("compose.send")}
              </button>
              <button
                type="button"
                onClick={openSchedule}
                className="theme-button h-10 w-10 rounded-l-none border-l border-white/30 p-0 disabled:opacity-60"
                disabled={send.isPending || saveDraft.isPending}
                aria-label="Send later"
                title="Send later"
              >
                <Clock3 size={17} />
              </button>
              </div>
          </div>
        </footer>
      </form>
      {canBrowseStorageAttachments && drivePickerOpen && driveStatus?.connected ? (
        <div className="absolute inset-0 z-20 grid place-items-end bg-slate-950/40 p-0 sm:place-items-center sm:p-4">
          <section className="grid h-[100dvh] w-full max-w-none gap-4 rounded-none border border-slate-200 bg-white p-4 shadow-soft sm:h-auto sm:max-w-2xl sm:rounded-xl sm:p-5">
            <div className="flex items-center justify-between gap-3">
              <div>
                <h3 className="text-lg font-black text-slate-950">{t("compose.pickFromDrive", { drive: driveServiceLabel })}</h3>
                <p className="text-sm font-semibold text-slate-500">{t("compose.driveHelp")}</p>
              </div>
              <button type="button" onClick={() => setDrivePickerOpen(false)} className="icon-button h-9 w-9 border-transparent" aria-label={t("compose.closeDrivePicker")}>
                <X size={18} />
              </button>
            </div>
            <div className="relative">
              <input value={driveSearch} onChange={(event) => setDriveSearch(event.target.value)} className="field h-11 w-full pr-10" placeholder={t("compose.searchDrive", { drive: driveServiceLabel })} />
              {driveSearch ? (
                <button
                  type="button"
                  onClick={() => setDriveSearch("")}
                  className="absolute right-2 top-1/2 inline-flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full text-slate-400 transition hover:bg-slate-200 hover:text-slate-700"
                  aria-label={t("compose.clearDriveSearch")}
                  title={t("compose.clearDriveSearch")}
                >
                  <X size={14} />
                </button>
              ) : null}
            </div>
            <div className="flex flex-wrap items-center gap-1 rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs font-semibold text-slate-500">
              <span className="font-black uppercase tracking-[0.12em] text-slate-400">{t("compose.breadcrumb")}</span>
              <button
                type="button"
                onClick={() => setDrivePickerFolderId("")}
                className={clsx("rounded-full px-2 py-0.5 transition", !drivePickerFolderId ? "bg-[color-mix(in_srgb,var(--accent)_10%,white)] text-[var(--accent)]" : "text-slate-600 hover:bg-slate-100")}
              >
                {driveRootLabel}
              </button>
              {drivePickerDisplayBreadcrumbs.map((crumb, index) => {
                const active = index === drivePickerDisplayBreadcrumbs.length - 1;
                return (
                  <div key={crumb.id} className="flex items-center gap-1">
                    <span className="text-slate-300">/</span>
                    <button
                      type="button"
                      onClick={() => setDrivePickerFolderId(crumb.id)}
                      className={clsx(
                        "rounded-full px-2 py-0.5 transition",
                        active ? "bg-[color-mix(in_srgb,var(--accent)_10%,white)] text-[var(--accent)]" : "text-slate-600 hover:bg-slate-100"
                      )}
                    >
                      {crumb.name}
                    </button>
                  </div>
                );
              })}
            </div>
            <div className="max-h-[calc(100dvh-220px)] overflow-auto rounded-xl border border-slate-200 sm:max-h-[55vh]">
              {driveFiles.isLoading ? <div className="p-5 text-sm font-semibold text-slate-500">{t("compose.loadingDriveFiles", { drive: driveServiceLabel })}</div> : null}
              {!driveFiles.isLoading && !(driveFiles.data?.folders.length ?? 0) && !(driveFiles.data?.files.length ?? 0) ? (
                <div className="grid gap-2 p-5 text-sm font-semibold text-slate-500">
                  <p>{t("compose.emptyFolder")}</p>
                  <p className="text-xs leading-6 text-slate-400">{t("compose.emptyFolderHelp")}</p>
                </div>
              ) : null}
              {drivePickerShowParentRow ? (
                <button
                  type="button"
                  onClick={() => setDrivePickerFolderId(drivePickerParentDestination)}
                  className="flex w-full items-center gap-3 border-t border-slate-100 px-4 py-3 text-left transition first:border-t-0 hover:bg-slate-50"
                >
                  <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-slate-100 text-slate-500">
                    <FolderIcon size={18} />
                  </div>
                  <div className="min-w-0 flex-1">
                    <p className="truncate text-sm font-bold text-slate-900">..</p>
                    <p className="text-xs font-semibold text-slate-500">{t("compose.parentFolder")}</p>
                  </div>
                </button>
              ) : null}
              {(driveFiles.data?.folders ?? []).map((folder) => (
                <button
                  key={folder.id}
                  type="button"
                  onClick={() => setDrivePickerFolderId(folder.id)}
                  className="flex w-full items-center gap-3 border-t border-slate-100 px-4 py-3 text-left transition first:border-t-0 hover:bg-slate-50"
                >
                  <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[color-mix(in_srgb,var(--accent)_10%,white)] text-[var(--accent)]">
                    <FolderIcon size={18} />
                  </div>
                  <div className="min-w-0 flex-1">
                    <p className="truncate text-sm font-bold text-slate-900">{folder.name}</p>
                    <p className="text-xs font-semibold text-slate-500">{t("compose.folderOpen")}</p>
                  </div>
                </button>
              ))}
              {(driveFiles.data?.files ?? []).map((file) => (
                <div key={file.id} className="flex flex-wrap items-center justify-between gap-3 border-t border-slate-100 px-4 py-3 first:border-t-0">
                  <div className="min-w-0">
                    <p className="truncate text-sm font-bold text-slate-900">{file.name}</p>
                    <p className="text-xs font-semibold text-slate-500">
                      {formatDriveMimeLabel(t, file.mime_type, file.name)} · {Math.max(1, Math.round(file.size / 1024))} KB
                    </p>
                  </div>
                  <div className="flex items-center gap-2">
                    <button type="button" onClick={() => attachDriveFile(file, "drive_file")} className="theme-button-soft h-9 px-3 text-sm">
                      {t("compose.attach")}
                    </button>
                    {allowDriveLink ? (
                      <button type="button" onClick={() => attachDriveFile(file, "drive_link")} className="theme-button h-9 px-3 text-sm">
                        {t("compose.link")}
                      </button>
                    ) : null}
                  </div>
                </div>
              ))}
            </div>
          </section>
        </div>
      ) : null}
      {confirmUnsavedClose ? (
        <div className="pointer-events-auto absolute inset-0 z-30 grid place-items-center bg-slate-950/30 p-4">
          <section className="w-full max-w-sm rounded-xl border border-slate-200 bg-white p-5 shadow-soft">
            <h3 className="text-lg font-black text-slate-950">{t("compose.unsavedChanges")}</h3>
            <p className="mt-2 text-sm leading-6 text-slate-600">{t("compose.unsavedHelp")}</p>
            <div className="mt-5 flex flex-wrap justify-end gap-2">
              <button type="button" onClick={() => setConfirmUnsavedClose(false)} className="h-10 rounded-xl border border-slate-200 bg-white px-4 font-bold text-slate-700 transition hover:bg-slate-50">
                {t("compose.keepEditing")}
              </button>
              <button
                type="button"
                onClick={() => {
                  setConfirmUnsavedClose(false);
                  if (saveCurrentDraft()) return;
                  setConfirmUnsavedClose(true);
                }}
                className="h-10 rounded-xl border border-slate-200 bg-slate-950 px-4 font-bold text-white transition hover:bg-slate-800"
              >
                {t("compose.saveDraft")}
              </button>
              <button
                type="button"
                onClick={() => {
                  setConfirmUnsavedClose(false);
                  onClose();
                }}
                className="h-10 rounded-xl border border-rose-200 bg-rose-50 px-4 font-bold text-rose-700 transition hover:bg-rose-100"
              >
                {t("compose.discard")}
              </button>
            </div>
          </section>
        </div>
      ) : null}
      {confirmNoSubject ? (
        <div className="pointer-events-auto absolute inset-0 z-30 grid place-items-center bg-slate-950/30 p-4">
          <section className="w-full max-w-sm rounded-xl border border-slate-200 bg-white p-5 shadow-soft">
            <h3 className="text-lg font-black text-slate-950">{t("compose.noSubject")}</h3>
            <p className="mt-2 text-sm leading-6 text-slate-600">{t("compose.noSubjectHelp")}</p>
            <div className="mt-5 flex justify-end gap-2">
              <button type="button" onClick={() => setConfirmNoSubject(false)} className="h-10 rounded-xl border border-slate-200 bg-white px-4 font-bold text-slate-700 transition hover:bg-slate-50">
                {t("compose.cancel")}
              </button>
              <button
                type="button"
                onClick={() => {
                  setConfirmNoSubject(false);
                  sendCurrentMessage(pendingSendAtMs);
                  setPendingSendAtMs(undefined);
                }}
                className="h-10 rounded-xl bg-slate-950 px-4 font-bold text-white transition hover:bg-slate-800"
              >
                {t("compose.sendAnyway")}
              </button>
            </div>
          </section>
        </div>
      ) : null}
      {scheduleOpen ? (
        <div className="pointer-events-auto absolute inset-0 z-30 grid place-items-center bg-slate-950/30 p-4">
          <section className="w-full max-w-sm rounded-xl border border-slate-200 bg-white p-5 shadow-soft">
            <div className="flex items-center justify-between gap-3">
              <h3 className="text-lg font-black text-slate-950">{t("compose.sendLater")}</h3>
              <button type="button" onClick={() => setScheduleOpen(false)} className="icon-button h-9 w-9 border-transparent" aria-label={t("compose.cancel")}>
                <X size={18} />
              </button>
            </div>
            <label className="mt-4 grid gap-2 text-sm font-bold text-slate-700">
              {t("compose.scheduleDateTime")}
              <input
                type="datetime-local"
                value={scheduledDateTime}
                onChange={(event) => {
                  setScheduledDateTime(event.target.value);
                  setScheduleError("");
                }}
                className="field h-11 w-full"
                autoFocus
              />
            </label>
            <p className="mt-2 text-xs font-semibold text-slate-500">{t("compose.scheduleTimeZone", { timeZone })}</p>
            {scheduleError ? <p className="mt-2 text-sm font-semibold text-red-600">{scheduleError}</p> : null}
            <div className="mt-5 flex justify-end gap-2">
              <button type="button" onClick={() => setScheduleOpen(false)} className="theme-button-soft h-10 px-4">{t("compose.cancel")}</button>
              <button type="button" onClick={scheduleMessage} className="theme-button h-10 px-4">
                <Clock3 size={17} />
                {t("compose.scheduleSend")}
              </button>
            </div>
          </section>
        </div>
      ) : null}
    </div>
  );
}

const RecipientField = forwardRef<RecipientFieldHandle, {
  label: string;
  displayLabel?: string;
  removeLabel: (address: string) => string;
  value: string;
  onChange: (value: string) => void;
  contacts: Contact[];
  required?: boolean;
}>(
function RecipientField({
  label,
  displayLabel,
  removeLabel,
  value,
  onChange,
  contacts,
  required = false
}, ref) {
  const [inputValue, setInputValue] = useState("");
  const [activeIndex, setActiveIndex] = useState(0);
  const inputRef = useRef<HTMLInputElement | null>(null);
  const addresses = useMemo(() => parseRecipientList(value), [value]);
  const token = inputValue.trim().toLowerCase();
  const suggestions = useMemo(
    () =>
      token.length
        ? [
            ...contacts
              .filter((contact) => {
                const email = contact.email.trim().toLowerCase();
                return email.includes(token) && !addresses.some((item) => item.toLowerCase() === email);
              })
              .slice(0, 6),
          ].filter((contact, index, items) => items.findIndex((item) => item.email.toLowerCase() === contact.email.toLowerCase()) === index)
        : [],
    [addresses, contacts, token]
  );

  useEffect(() => {
    setActiveIndex(0);
  }, [token, suggestions.length]);

  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current?.focus();
    },
    getValue() {
      const pending = (inputRef.current?.value ?? inputValue).trim();
      return pending ? recipientValue([...addresses, pending]) : value;
    },
  }), [addresses, inputValue, value]);

  function commitAddress(candidate: string) {
    const next = candidate.trim().replace(/,$/, "");
    if (!next) return;
    const exists = addresses.some((item) => item.toLowerCase() === next.toLowerCase());
    if (exists) {
      setInputValue("");
      return;
    }
    onChange(recipientValue([...addresses, next]));
    setInputValue("");
    setActiveIndex(0);
  }

  function chooseSuggestion(email: string) {
    const pending = (inputRef.current?.value ?? inputValue).trim();
    const nextAddresses = pending ? [...addresses, email] : [...addresses, email];
    const normalized = nextAddresses.map((item) => item.trim()).filter(Boolean);
    onChange(recipientValue(normalized));
    setInputValue("");
    setActiveIndex(0);
  }

  function removeAddress(index: number) {
    onChange(recipientValue(addresses.filter((_, itemIndex) => itemIndex !== index)));
  }

  function onBlur() {
    const pending = (inputRef.current?.value ?? inputValue).trim();
    if (pending) {
      commitAddress(pending);
    }
  }

  function onKeyDown(event: KeyboardEvent<HTMLInputElement>) {
    if ((event.key === "Enter" || event.key === "Tab" || event.key === ",") && inputValue.trim()) {
      event.preventDefault();
      if (suggestions.length) {
        chooseSuggestion(suggestions[activeIndex]?.email || suggestions[0].email);
      } else {
        commitAddress(inputValue);
      }
      return;
    }
    if (event.key === "Backspace" && !inputValue && addresses.length) {
      event.preventDefault();
      removeAddress(addresses.length - 1);
      return;
    }
    if (!suggestions.length) return;
    if (event.key === "ArrowDown") {
      event.preventDefault();
      setActiveIndex((current) => (current + 1) % suggestions.length);
      return;
    }
    if (event.key === "ArrowUp") {
      event.preventDefault();
      setActiveIndex((current) => (current - 1 + suggestions.length) % suggestions.length);
      return;
    }
    if (event.key === "Enter" || event.key === "Tab") {
      if (!token.length) return;
      event.preventDefault();
      chooseSuggestion(suggestions[activeIndex]?.email || suggestions[0].email);
      return;
    }
    if (event.key === "Escape") {
      setActiveIndex(0);
    }
  }

  return (
    <div className="relative">
      <label className="mb-1 block text-sm font-bold text-slate-600">{displayLabel || label}</label>
      <div className="field min-h-11 w-full px-3 py-2">
        <div className="flex flex-wrap items-center gap-2">
          {addresses.map((address, index) => (
            <span key={`${label}-${address}-${index}`} className="inline-flex items-center gap-2 rounded-full bg-slate-100 px-3 py-1 text-xs font-bold text-slate-700">
              <span className="max-w-[220px] truncate">{address}</span>
              <button
                type="button"
                onClick={() => removeAddress(index)}
                className="inline-flex h-4 w-4 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-200 hover:text-slate-700"
                aria-label={removeLabel(address)}
              >
                <X size={10} />
              </button>
            </span>
          ))}
          <input
            ref={inputRef}
            name={label.toLowerCase()}
            value={inputValue}
            onChange={(event) => setInputValue(event.target.value)}
            onKeyDown={onKeyDown}
            onBlur={onBlur}
            className="min-w-[180px] flex-1 bg-transparent text-sm outline-none placeholder:text-slate-400"
            placeholder={addresses.length ? "" : label}
            required={required && !addresses.length}
            autoComplete="off"
          />
        </div>
      </div>
      {suggestions.length ? (
        <div className="absolute left-0 top-full z-20 mt-1 w-full overflow-hidden rounded-xl border border-slate-200 bg-white shadow-soft sm:max-w-md">
          {suggestions.map((contact) => (
            <button
              key={`${label}-${contact.id || contact.email}`}
              type="button"
              onMouseDown={(event) => event.preventDefault()}
              onClick={() => chooseSuggestion(contact.email)}
              className={`flex w-full items-center gap-3 px-3 py-2 text-left text-sm transition hover:bg-slate-50 ${suggestions[activeIndex]?.id === contact.id ? "bg-slate-50" : ""}`}
            >
              <ContactAvatar value={contact.full_name || contact.email} photoUrl={contact.photo_url} />
              <span className="min-w-0">
                <strong className="block truncate text-slate-800">{contact.email}</strong>
              </span>
            </button>
          ))}
        </div>
      ) : null}
    </div>
  );
});

function htmlToText(value: string) {
  if (typeof document === "undefined") return value.replace(/<[^>]*>/g, "");
  const element = document.createElement("div");
  element.innerHTML = value;
  return editorPlainText(element);
}

function editorPlainText(editor: HTMLElement | null) {
  if (!editor) return "";
  const clone = editor.cloneNode(true) as HTMLElement;
  clone.querySelectorAll("img").forEach((image) => image.remove());
  return clone.innerText.trim();
}

function FormatButton({
  label,
  icon,
  onClick,
  dataAttachmentAction = false,
}: {
  label: string;
  icon: ReactNode;
  onClick: () => void;
  dataAttachmentAction?: boolean;
}) {
  return (
    <button
      type="button"
      onMouseDown={(event) => event.preventDefault()}
      onClick={onClick}
      data-attachment-action={dataAttachmentAction ? "true" : undefined}
      className="inline-flex h-8 w-8 items-center justify-center rounded-md text-slate-600 transition hover:bg-white hover:text-slate-950"
      aria-label={label}
      title={label}
    >
      {icon}
    </button>
  );
}

function ColorButton({ label, icon, onChange }: { label: string; icon: ReactNode; onChange: (color: string) => void }) {
  return (
    <label
      className="relative inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-slate-600 transition hover:bg-white hover:text-slate-950"
      aria-label={label}
      title={label}
    >
      {icon}
      <input
        type="color"
        className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
        onChange={(event) => onChange(event.target.value)}
      />
    </label>
  );
}
