"use client";

import { useRouter } from "next/navigation";
import {
  forwardRef,
  useEffect,
  useEffectEvent,
  useId,
  useImperativeHandle,
  useLayoutEffect,
  useRef,
  useState,
  type FormEvent,
  type ReactNode,
} from "react";
import { createPortal } from "react-dom";
import { submitMailboxMessageAction } from "@/app/actions/session";
import { CloseIconButton } from "@/components/close-icon-button";
import {
  MailComposeAddressBookDialog,
  type ComposeAddressBookSelection,
} from "@/components/mail-compose-address-book-dialog";
import { useI18n } from "@/components/i18n-provider";
import { MailComposeSendingOverlay } from "@/components/mail-compose-sending-overlay";
import { useComposeExitSave } from "@/components/use-compose-exit-save";
import {
  FREE_REGULAR_ATTACHMENT_LIMIT_BYTES,
  GROWTH_REGULAR_ATTACHMENT_LIMIT_BYTES,
  canRestoreRegularAttachment,
  initialAttachmentMode,
  resolveComposeSubject,
  type ComposeAttachmentMode,
} from "@/lib/mail-attachment-policy";
import type { TranslationKey } from "@/lib/i18n-dictionaries";
import { MAIL_QUOTE_BASE_CSS } from "@/lib/mail-quote-style";
import type { MailMessageAttachment } from "@/lib/official-mail";

type UiTranslator = (
  key: TranslationKey,
  values?: Record<string, string | number>,
) => string;

type MailComposeFormProps = {
  growth: boolean;
  mailboxAddress: string;
  composeMode: "draft" | "forward" | "new" | "reply" | "replyAll";
  sourceDraftMessageId: number | null;
  draftAttachments: MailMessageAttachment[];
  currentFolder: string;
  defaultBcc: string;
  defaultBody: string;
  defaultBodyHtml: string;
  defaultCc: string;
  defaultSubject: string;
  defaultTo: string;
  signatureHtml: string;
  signatureText: string;
};

type RecipientFieldProps = {
  autoFocus?: boolean;
  defaultValue: string;
  endAction?: ReactNode;
  errorMessage?: string;
  focusSignal?: number;
  inputId: string;
  label: string;
  name: string;
  onInteract?: () => void;
  onSendIndividuallyChange?: (checked: boolean) => void;
  placeholder: string;
  sendIndividually?: boolean;
};

type RecipientFieldHandle = {
  getRecipients: () => ComposeRecipientToken[];
  replaceRecipients: (recipients: ComposeRecipientToken[]) => void;
};

type ComposeRecipientToken = {
  email: string;
  label: string;
};

type ComposeRecipientSuggestion = {
  displayName: string | null;
  email: string;
  kind: "history" | "member";
  label: string;
};

type ComposeUploadKind = "attachment" | "inline-image";

type ComposeUploadItem = {
  contentType: string;
  filename: string;
  id: number;
  isImage: boolean;
  kind: ComposeUploadKind;
  previewUrl: string;
  sizeBytes: number;
  token: string;
};

type ComposeLocalAttachment = {
  contentType: string;
  file: File;
  filename: string;
  id: string;
  isImage: boolean;
  sizeBytes: number;
  mode: ComposeAttachmentMode;
};

type ComposeUploadProgressState = {
  currentFileName: string;
  kind: ComposeUploadKind;
  percent: number;
  totalFiles: number;
};

type ComposeSendUploadStatus = "complete" | "failed" | "uploading" | "waiting";

type ComposeSendProgressItem = {
  filename: string;
  id: number | string;
  kind: ComposeUploadKind;
  sizeBytes: number;
  status: ComposeSendUploadStatus;
  uploadedBytes: number;
};

type ComposeSendProgressSnapshot = {
  completedBytes: number;
  items: ComposeSendProgressItem[];
  totalBytes: number;
  totalCount: number;
  uploadedCount: number;
};

type ComposeIndividualSendProgress = {
  completedCount: number;
  totalCount: number;
};

type ComposeSendModalState =
  | {
      individualProgress?: ComposeIndividualSendProgress;
      snapshot: ComposeSendProgressSnapshot;
      stage: "complete" | "sending" | "uploading";
    }
  | {
      errorMessage: string;
      individualProgress?: ComposeIndividualSendProgress;
      snapshot: ComposeSendProgressSnapshot;
      stage: "error";
    };

type ComposeValidationErrors = {
  body?: string;
  recipient?: string;
  subject?: string;
};

type HtmlEditorViewportSize = "small" | "medium" | "large";
type HtmlEditorViewMode = "source" | "visual";

type HtmlLinkDialogState = {
  href: string;
  mode: "create" | "edit";
  text: string;
};

type HtmlLinkPopoverState = {
  href: string;
  left: number;
  top: number;
};

const PRESERVED_HTML_EDITOR_DOCUMENT = `<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style>
      html { min-height: 100%; background: #fff; }
      body {
        box-sizing: border-box;
        min-height: 520px;
        margin: 0;
        padding: 20px;
        overflow-x: auto;
        background: #fff;
        color: #223250;
        font-family: Pretendard, "Malgun Gothic", Arial, sans-serif;
        font-size: 15px;
        line-height: 1.8;
        caret-color: #2f6df6;
        outline: none;
      }
      body:focus { outline: none; }
      table, img { max-width: 100%; }
      ${MAIL_QUOTE_BASE_CSS}
      a { color: #2f6df6; cursor: pointer; text-decoration: underline; }
    </style>
  </head>
  <body></body>
</html>`;

function splitRecipientChunks(value: string) {
  return value
    .split(/[,;\r\n\t]+/)
    .map((item) => item.trim())
    .filter(Boolean);
}

function hasRecipientSeparator(value: string) {
  return /[,;\r\n\t]/.test(value);
}

function extractRecipientEmail(value: string) {
  const trimmed = value.trim();

  if (!trimmed) {
    return "";
  }

  const bracketMatch = trimmed.match(/<([^<>]+)>$/);

  if (bracketMatch) {
    return bracketMatch[1].trim().toLowerCase();
  }

  return trimmed.toLowerCase();
}

function buildRecipientLabel(email: string, displayName?: string | null) {
  const normalizedName = displayName?.trim() ?? "";
  return normalizedName ? `${normalizedName} <${email}>` : email;
}

function createRecipientToken(
  value: string,
  input?: {
    displayName?: string | null;
    label?: string;
  },
) {
  const email = extractRecipientEmail(value);

  if (!email) {
    return null;
  }

  return {
    email,
    label: input?.label?.trim() || buildRecipientLabel(email, input?.displayName),
  } satisfies ComposeRecipientToken;
}

function splitRecipientTokens(value: string) {
  return splitRecipientChunks(value).flatMap((item) => {
    const token = createRecipientToken(item, {
      label: item,
    });

    return token ? [token] : [];
  });
}

function splitRecipients(value: string) {
  return splitRecipientTokens(value).map((token) => token.email);
}

function mergeRecipientTokens(current: ComposeRecipientToken[], incoming: ComposeRecipientToken[]) {
  const seen = new Set(current.map((token) => token.email));
  const next = [...current];

  incoming.forEach((recipient) => {
    if (!seen.has(recipient.email)) {
      seen.add(recipient.email);
      next.push(recipient);
    }
  });

  return next;
}

function serializeRecipientEmails(tokens: ComposeRecipientToken[], draft: string) {
  return mergeRecipientTokens(tokens, splitRecipientTokens(draft))
    .map((token) => token.email)
    .join(", ");
}

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

function plainTextToHtml(value: string) {
  const lines = value.replace(/\r/g, "").split("\n");

  if (lines.length === 0) {
    return "<p><br></p>";
  }

  return lines
    .map((line) => (line.trim() ? `<p>${escapeHtml(line)}</p>` : "<p><br></p>"))
    .join("");
}

function ensureLeadingComposeSpaceText(value: string) {
  if (!value.trim()) {
    return "";
  }

  return value.startsWith("\n") ? value : `\n${value}`;
}

function ensureLeadingComposeSpaceHtml(value: string) {
  const normalized = value.trim();

  if (!normalized) {
    return "";
  }

  return normalized.startsWith("<p><br></p>") ? normalized : `<p><br></p>${normalized}`;
}

function insertComposeSignatureHtml(input: {
  bodyHtml: string;
  composeMode: MailComposeFormProps["composeMode"];
  signatureHtml: string;
}) {
  const bodyHtml = input.bodyHtml.trim();
  const signatureHtml = input.signatureHtml.trim();

  if (!signatureHtml || input.composeMode === "draft") {
    return bodyHtml;
  }

  if (input.composeMode === "reply" || input.composeMode === "replyAll" || input.composeMode === "forward") {
    return bodyHtml ? `${signatureHtml}<p><br></p>${bodyHtml}` : signatureHtml;
  }

  return bodyHtml ? `${bodyHtml}<p><br></p>${signatureHtml}` : `<p><br></p>${signatureHtml}`;
}

function insertComposeSignatureText(input: {
  bodyText: string;
  composeMode: MailComposeFormProps["composeMode"];
  signatureText: string;
}) {
  const bodyText = input.bodyText.replace(/\r/g, "").trim();
  const signatureText = input.signatureText.replace(/\r/g, "").trim();

  if (!signatureText || input.composeMode === "draft") {
    return bodyText;
  }

  if (input.composeMode === "reply" || input.composeMode === "replyAll" || input.composeMode === "forward") {
    return bodyText ? `${signatureText}\n\n${bodyText}` : signatureText;
  }

  return bodyText ? `${bodyText}\n\n${signatureText}` : `\n${signatureText}`;
}

function normalizeEditorText(value: string) {
  return value
    .replace(/\r/g, "")
    .replace(/\u00a0/g, " ")
    .replace(/\n{3,}/g, "\n\n")
    .trim();
}

function htmlToEditorText(value: string) {
  if (typeof DOMParser === "undefined") {
    return normalizeEditorText(value.replace(/<[^>]*>/g, " "));
  }

  const documentNode = new DOMParser().parseFromString(value, "text/html");
  return normalizeEditorText(documentNode.body.innerText || documentNode.body.textContent || "");
}

function sanitizePastedEmailCss(value: string) {
  if (/@import|url\s*\(|expression\s*\(|javascript\s*:|behavior\s*:|-moz-binding/i.test(value)) {
    return "";
  }

  return value.replace(/<\/?style\b[^>]*>/gi, "").trim();
}

function isAllowedComposeImageSource(value: string) {
  if (/^(?:https?:|data:image\/|cid:|\/api\/mailbox\/uploads\/)/i.test(value)) {
    return true;
  }

  if (!value.startsWith("/api/mailbox/attachment?")) {
    return false;
  }

  const source = new URL(value, "https://officialmail.invalid");
  const messageId = Number(source.searchParams.get("message"));
  const attachmentIndex = Number(source.searchParams.get("index"));

  return source.pathname === "/api/mailbox/attachment" &&
    Number.isInteger(messageId) &&
    messageId > 0 &&
    Number.isInteger(attachmentIndex) &&
    source.searchParams.get("mode") === "inline";
}

function sanitizePastedInlineStyle(value: string) {
  return value
    .split(";")
    .map((declaration) => declaration.trim())
    .filter(
      (declaration) =>
        declaration &&
        !/url\s*\(|expression\s*\(|javascript\s*:|behavior\s*:|-moz-binding/i.test(declaration),
    )
    .join(";");
}

function prepareEmailHtml(value: string) {
  if (typeof DOMParser === "undefined") {
    return null;
  }

  const documentNode = new DOMParser().parseFromString(value, "text/html");
  const existingWrapper = documentNode.body.querySelector<HTMLElement>(
    ':scope > [data-compose-layout="preserved-html"]',
  );

  documentNode
    .querySelectorAll("script,iframe,object,embed,form,input,textarea,select,button,link,base,meta,noscript")
    .forEach((node) => node.remove());

  documentNode.querySelectorAll<HTMLElement>("*").forEach((element) => {
    Array.from(element.attributes).forEach((attribute) => {
      if (/^on/i.test(attribute.name) || ["srcdoc", "formaction"].includes(attribute.name.toLowerCase())) {
        element.removeAttribute(attribute.name);
      }
    });

    const inlineStyle = element.getAttribute("style");
    if (inlineStyle) {
      const safeStyle = sanitizePastedInlineStyle(inlineStyle);
      if (safeStyle) {
        element.setAttribute("style", safeStyle);
      } else {
        element.removeAttribute("style");
      }
    }

    if (element instanceof HTMLAnchorElement) {
      const href = element.getAttribute("href")?.trim() ?? "";
      if (href && !/^(?:https?:|mailto:|#)/i.test(href)) {
        element.removeAttribute("href");
      }
      element.setAttribute("rel", "noopener noreferrer");
      element.setAttribute("target", "_blank");
    }

    if (element instanceof HTMLImageElement) {
      const src = element.getAttribute("src")?.trim() ?? "";
      if (src && !isAllowedComposeImageSource(src)) {
        element.removeAttribute("src");
      }
    }
  });

  documentNode.querySelectorAll<HTMLTableElement>("table").forEach((table) => {
    const declaredWidth = table.getAttribute("width")?.trim() ?? "";

    if (/^\d+(?:\.\d+)?%$/.test(declaredWidth)) {
      table.style.width = declaredWidth;
    }

    if (table.classList.contains("email-shell")) {
      table.setAttribute("width", "100%");
      table.style.width = "100%";
    }
  });

  const styleSheets = Array.from(documentNode.querySelectorAll("style"))
    .map((style) => sanitizePastedEmailCss(style.textContent ?? ""))
    .filter(Boolean);
  documentNode.querySelectorAll("style").forEach((style) => style.remove());

  const bodyText = normalizeEditorText(documentNode.body.innerText || documentNode.body.textContent || "");

  if (existingWrapper && documentNode.body.children.length === 1) {
    return {
      html: `${styleSheets.map((css) => `<style>${css}</style>`).join("")}${existingWrapper.outerHTML}`,
      text: bodyText,
    };
  }

  const bodyStyle = sanitizePastedInlineStyle(documentNode.body.getAttribute("style") ?? "");
  const styleAttribute = bodyStyle ? ` style="${escapeHtml(bodyStyle)}"` : "";
  const styleHtml = styleSheets.map((css) => `<style>${css}</style>`).join("");

  return {
    html: `${styleHtml}<div data-compose-layout="preserved-html"${styleAttribute}>${documentNode.body.innerHTML}</div>`,
    text: bodyText,
  };
}

function prepareEditableEmailHtml(value: string) {
  return prepareEmailHtml(value);
}

function getPreservedHtmlEditorSource(value: string) {
  const wrapperStart = /<div\b(?=[^>]*\bdata-compose-layout\s*=\s*["']preserved-html["'])[^>]*>/i.exec(
    value,
  );

  if (!wrapperStart || wrapperStart.index === undefined) {
    return value;
  }

  const wrapperEndIndex = value.toLowerCase().lastIndexOf("</div>");
  const contentStartIndex = wrapperStart.index + wrapperStart[0].length;

  if (wrapperEndIndex < contentStartIndex || value.slice(wrapperEndIndex + 6).trim()) {
    return value;
  }

  return `${value.slice(0, wrapperStart.index)}${value.slice(contentStartIndex, wrapperEndIndex)}`;
}

function wrapPreservedHtmlEditorSource(value: string) {
  return (
    prepareEditableEmailHtml(value)?.html ??
    `<div data-compose-layout="preserved-html">${value}</div>`
  );
}

function formatUploadSize(sizeBytes: number) {
  if (sizeBytes < 1024) {
    return `${sizeBytes} B`;
  }

  if (sizeBytes < 1024 * 1024) {
    return `${(sizeBytes / 1024).toFixed(sizeBytes < 10 * 1024 ? 1 : 0)} KB`;
  }

  return `${(sizeBytes / (1024 * 1024)).toFixed(sizeBytes < 10 * 1024 * 1024 ? 1 : 0)} MB`;
}

function mergeUploads(current: ComposeUploadItem[], incoming: ComposeUploadItem[]) {
  const map = new Map(current.map((upload) => [upload.id, upload]));

  incoming.forEach((upload) => {
    map.set(upload.id, upload);
  });

  return [...map.values()];
}

function createLocalAttachmentId() {
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
    return crypto.randomUUID();
  }

  return `attachment-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}

function toLocalAttachment(file: File, mode: ComposeAttachmentMode): ComposeLocalAttachment {
  const isImage = file.type.startsWith("image/");

  return {
    contentType: file.type || "application/octet-stream",
    file,
    filename: file.name || "attachment",
    id: createLocalAttachmentId(),
    isImage,
    sizeBytes: file.size,
    mode,
  };
}

function mergeLocalAttachments(
  current: ComposeLocalAttachment[],
  incoming: ComposeLocalAttachment[],
) {
  if (incoming.length === 0) {
    return current;
  }

  return [...current, ...incoming];
}

function clampProgressRatio(value: number) {
  if (!Number.isFinite(value)) {
    return 0;
  }

  return Math.min(1, Math.max(0, value));
}

function buildComposeUploadProgressState(
  kind: ComposeUploadKind,
  files: File[],
  progressRatio: number,
  fallbackLabel: string,
): ComposeUploadProgressState {
  const safeFiles = files.filter((file) => file.size >= 0);
  const totalBytes = safeFiles.reduce((sum, file) => sum + file.size, 0);
  const normalizedRatio = clampProgressRatio(progressRatio);
  const loadedBytes = totalBytes * normalizedRatio;
  const fallbackName = safeFiles[safeFiles.length - 1]?.name || fallbackLabel;
  let cumulativeBytes = 0;
  let currentFileName = fallbackName;

  for (const file of safeFiles) {
    cumulativeBytes += file.size;

    if (loadedBytes < cumulativeBytes || file === safeFiles[safeFiles.length - 1]) {
      currentFileName = file.name || fallbackName;
      break;
    }
  }

  return {
    currentFileName,
    kind,
    percent: Math.round(normalizedRatio * 100),
    totalFiles: Math.max(1, safeFiles.length),
  };
}

function parseComposeUploadResponse(xhr: XMLHttpRequest) {
  const response = xhr.response;

  if (response && typeof response === "object") {
    return response as { error?: string; uploads?: ComposeUploadItem[] };
  }

  if (typeof xhr.responseText === "string" && xhr.responseText.trim()) {
    try {
      return JSON.parse(xhr.responseText) as { error?: string; uploads?: ComposeUploadItem[] };
    } catch {
      return null;
    }
  }

  return null;
}

function getComposeUploadProgressTitle(kind: ComposeUploadKind, t: UiTranslator) {
  return kind === "inline-image"
    ? t("compose.file.uploading.inline")
    : t("compose.file.uploading.attachment");
}

function createEmptyComposeSendProgressSnapshot(): ComposeSendProgressSnapshot {
  return {
    completedBytes: 0,
    items: [],
    totalBytes: 0,
    totalCount: 0,
    uploadedCount: 0,
  };
}

function getComposeSendErrorMessage(redirectTo: string, t: UiTranslator) {
  const errorUrl = new URL(redirectTo, window.location.origin);
  const errorCode = errorUrl.searchParams.get("error");
  const details = errorUrl.searchParams.get("details")?.trim();

  switch (errorCode) {
    case "compose-required":
      return t("compose.error.required");
    case "invalid-recipient":
      return t("compose.error.invalidRecipient");
    case "compose-subject-too-long":
      return t("compose.error.subjectTooLong");
    case "compose-input-too-long":
      return t("compose.error.inputTooLong");
    case "compose-queue-busy":
      return t("compose.error.queueBusy");
    case "compose-request-too-large":
      return t("compose.error.requestTooLarge");
    case "attachment-free-limit":
      return t("compose.error.freeAttachmentLimit");
    case "attachment-large-limit":
      return t("compose.error.largeAttachmentLimit");
    case "attachment-growth-required":
      return t("compose.error.growthAttachmentRequired");
    case "attachment-drawer-quota":
      return t("compose.error.drawerQuota");
    case "compose-service-unavailable":
      return t("compose.error.serviceUnavailable");
    case "free-send-limit-daily":
      return t("compose.error.dailyLimit");
    case "send-cooldown-active":
      return details || t("compose.error.cooldown");
    case "smtp-failed":
      return details ? `${t("compose.error.default")} ${details}` : t("compose.error.default");
    case "imap-failed":
      return details
        ? `${t("compose.error.imap")} ${details}`
        : t("compose.error.imap");
    default: {
      const requestId = errorUrl.searchParams.get("request")?.trim();
      return `${t("compose.error.server")}${requestId ? ` (${requestId})` : ""}`;
    }
  }
}

function getComposeRequestFailureMessage(error: unknown, t: UiTranslator) {
  const code = error instanceof Error ? error.message : "";

  if (code === "mail-send-request-too-large") {
    return t("compose.error.requestTooLarge");
  }

  if (code === "mail-send-growth-required") {
    return t("compose.error.growthAttachmentRequired");
  }

  if (code === "mail-send-drawer-quota") {
    return t("compose.error.drawerQuota");
  }

  if (code === "mail-send-service-unavailable") {
    return t("compose.error.serviceUnavailable");
  }

  if (code === "mail-send-request-aborted") {
    return t("compose.error.aborted");
  }

  if (code === "mail-send-response-invalid") {
    return t("compose.error.responseInvalid");
  }

  return t("compose.error.connection");
}

function buildComposeSendSnapshotFromLocalAttachments(
  attachments: ComposeLocalAttachment[],
): ComposeSendProgressSnapshot {
  const items = attachments.map((attachment) => ({
    filename: attachment.filename,
    id: attachment.id,
    kind: "attachment" as const,
    sizeBytes: attachment.sizeBytes,
    status: "waiting" as const,
    uploadedBytes: 0,
  }));

  return {
    completedBytes: 0,
    items,
    totalBytes: items.reduce((sum, item) => sum + item.sizeBytes, 0),
    totalCount: items.length,
    uploadedCount: 0,
  };
}

function applyFileUploadProgress(
  snapshot: ComposeSendProgressSnapshot,
  uploadedById: Map<string, number>,
): ComposeSendProgressSnapshot {
  const items = snapshot.items.map((item) => {
    const uploadedBytes = Math.min(item.sizeBytes, Math.max(0, uploadedById.get(String(item.id)) ?? 0));
    return {
      ...item,
      status: uploadedBytes >= item.sizeBytes
        ? "complete" as const
        : uploadedBytes > 0 ? "uploading" as const : "waiting" as const,
      uploadedBytes,
    };
  });
  return {
    completedBytes: items.reduce((sum, item) => sum + item.uploadedBytes, 0),
    items,
    totalBytes: snapshot.totalBytes,
    totalCount: snapshot.totalCount,
    uploadedCount: items.filter((item) => item.status === "complete").length,
  };
}

function markIncompleteComposeUploadsFailed(
  snapshot: ComposeSendProgressSnapshot,
): ComposeSendProgressSnapshot {
  return {
    ...snapshot,
    items: snapshot.items.map((item) => item.status === "complete"
      ? item
      : { ...item, status: "failed" as const }),
  };
}

function getComposeSendUploadStatusLabel(status: ComposeSendUploadStatus, t: UiTranslator) {
  if (status === "complete") {
    return t("compose.complete");
  }

  if (status === "failed") {
    return t("compose.upload.failedShort");
  }

  if (status === "uploading") {
    return t("compose.uploading");
  }

  return t("compose.upload.waiting");
}

function getComposeSendUploadStatusTone(status: ComposeSendUploadStatus) {
  if (status === "complete") {
    return "text-[#17a34a]";
  }

  if (status === "failed") {
    return "text-[#c54848]";
  }

  if (status === "uploading") {
    return "text-[#2f6df6]";
  }

  return "text-[#7f8ea7]";
}

function buildComposeTitle(
  composeMode: MailComposeFormProps["composeMode"],
  t: UiTranslator,
) {
  if (composeMode === "draft") {
    return t("compose.draft.edit");
  }

  if (composeMode === "reply") {
    return t("compose.reply");
  }

  if (composeMode === "replyAll") {
    return t("compose.replyAll");
  }

  if (composeMode === "forward") {
    return t("compose.forward");
  }

  return t("compose.new");
}

function normalizeComposeLinkHref(value: string) {
  const trimmed = value.trim();

  if (!trimmed) {
    return null;
  }

  const normalized = /^(?:https?:\/\/|mailto:|#)/i.test(trimmed)
    ? trimmed
    : `https://${trimmed}`;

  if (normalized.startsWith("#")) {
    return normalized;
  }

  if (/^mailto:[^\s@]+@[^\s@]+\.[^\s@]+$/i.test(normalized)) {
    return normalized;
  }

  try {
    const parsed = new URL(normalized);
    return /^https?:$/i.test(parsed.protocol) ? parsed.href : null;
  } catch {
    return null;
  }
}

const composeActionGroupClassName =
  "absolute right-3 top-1/2 inline-flex shrink-0 -translate-y-1/2 items-center justify-end gap-2 whitespace-nowrap sm:right-4 lg:right-8";

const composePrimaryActionButtonClassName =
  "inline-flex h-8 shrink-0 items-center justify-center rounded-[10px] bg-[#2f6df6] px-3 text-[12px] font-semibold text-white transition-colors duration-150 hover:bg-[#245de3] disabled:opacity-60 sm:min-w-[76px]";

const composeSecondaryActionButtonClassName =
  "inline-flex h-8 shrink-0 items-center justify-center rounded-[10px] border border-[#d7e3fb] bg-white px-3 text-[12px] font-semibold text-[#526784] transition-colors duration-150 hover:border-[#bfd4ff] hover:bg-[#f7faff] hover:text-[#245de3] disabled:cursor-not-allowed disabled:opacity-60 sm:min-w-[88px]";

const defaultComposeTextColor = "#223250";
const defaultComposeBackgroundColor = "#fff2b3";

function AttachmentIcon({ isImage }: { isImage: boolean }) {
  if (isImage) {
    return (
      <svg viewBox="0 0 24 24" fill="none" className="h-[18px] w-[18px]">
        <path
          d="M5 6.5A1.5 1.5 0 0 1 6.5 5h11A1.5 1.5 0 0 1 19 6.5v11a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 5 17.5Z"
          stroke="currentColor"
          strokeWidth="1.8"
        />
        <path d="M8 15.5 10.8 12.7 13.2 15.1 15.6 12.7 18 15.1" stroke="currentColor" strokeWidth="1.8" />
        <circle cx="9" cy="9" r="1.2" fill="currentColor" />
      </svg>
    );
  }

  return (
    <svg viewBox="0 0 24 24" fill="none" className="h-[18px] w-[18px]">
      <path
        d="M7 4.8h7l3 3V19a1.8 1.8 0 0 1-1.8 1.8H7A1.8 1.8 0 0 1 5.2 19V6.6A1.8 1.8 0 0 1 7 4.8Z"
        stroke="currentColor"
        strokeWidth="1.8"
      />
      <path d="M14 4.8v3.5h3.5" stroke="currentColor" strokeWidth="1.8" />
      <path d="M8.5 12h7" stroke="currentColor" strokeWidth="1.8" />
      <path d="M8.5 15.5h5.5" stroke="currentColor" strokeWidth="1.8" />
    </svg>
  );
}

function HtmlViewportIcon({ size }: { size: HtmlEditorViewportSize }) {
  const contentWidth = size === "small" ? 6 : size === "medium" ? 11 : 16;
  const contentX = 10 - contentWidth / 2;

  return (
    <svg viewBox="0 0 20 20" fill="none" className="h-[18px] w-[18px]" aria-hidden="true">
      <rect
        x="1.5"
        y="3"
        width="17"
        height="14"
        rx="2.5"
        stroke="currentColor"
        strokeWidth="1.4"
        opacity="0.38"
      />
      <rect
        x={contentX}
        y="5"
        width={contentWidth}
        height="10"
        rx="1.5"
        fill="currentColor"
      />
    </svg>
  );
}

const RecipientAutocompleteField = forwardRef<RecipientFieldHandle, RecipientFieldProps>(
  function RecipientAutocompleteField(
    {
      autoFocus = false,
      defaultValue,
      endAction,
      errorMessage,
      focusSignal = 0,
      inputId,
      label,
      name,
      onInteract,
      onSendIndividuallyChange,
      placeholder,
      sendIndividually = false,
    },
    ref,
  ) {
  const { t } = useI18n();
  const suggestionListId = useId().replace(/:/g, "");
  const errorMessageId = `${inputId}-error`;
  const [tokens, setTokens] = useState(() => splitRecipientTokens(defaultValue));
  const [draft, setDraft] = useState("");
  const [suggestions, setSuggestions] = useState<ComposeRecipientSuggestion[]>([]);
  const [suggestionsOpen, setSuggestionsOpen] = useState(false);
  const [highlightedIndex, setHighlightedIndex] = useState(-1);
  const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
  const [isFocused, setIsFocused] = useState(false);
  const [suggestionsDismissed, setSuggestionsDismissed] = useState(false);
  const [individualHelpOpen, setIndividualHelpOpen] = useState(false);
  const inputRef = useRef<HTMLInputElement | null>(null);
  const abortRef = useRef<AbortController | null>(null);

  useImperativeHandle(
    ref,
    () => ({
      getRecipients: () => mergeRecipientTokens(tokens, splitRecipientTokens(draft)),
      replaceRecipients: (recipients) => {
        onInteract?.();
        setTokens(mergeRecipientTokens([], recipients));
        setDraft("");
        setSuggestions([]);
        setSuggestionsOpen(false);
        setHighlightedIndex(-1);
      },
    }),
    [draft, onInteract, tokens],
  );

  const focusInput = () => {
    window.requestAnimationFrame(() => {
      inputRef.current?.focus();
      const valueLength = inputRef.current?.value.length ?? 0;
      inputRef.current?.setSelectionRange(valueLength, valueLength);
    });
  };

  const commitDraft = () => {
    if (!draft.trim()) {
      setIsLoadingSuggestions(false);
      setSuggestions([]);
      setSuggestionsOpen(false);
      setHighlightedIndex(-1);
      return;
    }

    onInteract?.();
    setTokens((current) => mergeRecipientTokens(current, splitRecipientTokens(draft)));
    setDraft("");
    setIsLoadingSuggestions(false);
    setSuggestions([]);
    setSuggestionsOpen(false);
    setHighlightedIndex(-1);
  };

  const selectSuggestion = (suggestion: ComposeRecipientSuggestion) => {
    const token = createRecipientToken(suggestion.email, {
      displayName: suggestion.displayName,
      label: suggestion.label,
    });

    if (!token) {
      return;
    }

    onInteract?.();
    setTokens((current) => mergeRecipientTokens(current, [token]));
    setDraft("");
    setIsLoadingSuggestions(false);
    setSuggestions([]);
    setSuggestionsOpen(false);
    setSuggestionsDismissed(true);
    setHighlightedIndex(-1);
    focusInput();
  };

  const editToken = (index: number) => {
    setTokens((current) => {
      const next = [...current];
      const [removed] = next.splice(index, 1);
      setDraft(removed?.email ?? "");
      return next;
    });
    focusInput();
  };

  const removeToken = (index: number) => {
    onInteract?.();
    setTokens((current) => current.filter((_, tokenIndex) => tokenIndex !== index));
    focusInput();
  };

  useEffect(() => {
    if (focusSignal <= 0) {
      return undefined;
    }

    const frameId = window.requestAnimationFrame(() => {
      inputRef.current?.focus();
    });

    return () => window.cancelAnimationFrame(frameId);
  }, [focusSignal]);

  useEffect(() => {
    const normalizedDraft = draft.trim();

    abortRef.current?.abort();
    abortRef.current = null;

    if (!isFocused || suggestionsDismissed || hasRecipientSeparator(draft)) {
      return;
    }

    const controller = new AbortController();
    abortRef.current = controller;
    const timeoutId = window.setTimeout(async () => {
      setIsLoadingSuggestions(true);
      setSuggestions([]);
      setHighlightedIndex(-1);
      try {
        const response = await fetch(
          `/api/mailbox/recipients?${new URLSearchParams({ q: normalizedDraft }).toString()}`,
          {
            credentials: "same-origin",
            signal: controller.signal,
          },
        );

        if (!response.ok) {
          throw new Error("recipient-suggestions-failed");
        }

        const payload = (await response.json()) as {
          suggestions?: ComposeRecipientSuggestion[];
        };
        if (controller.signal.aborted || abortRef.current !== controller) return;
        const nextSuggestions = Array.isArray(payload.suggestions)
          ? payload.suggestions.filter(
              (suggestion) => !tokens.some((token) => token.email === suggestion.email),
            ).slice(0, 10)
          : [];

        setSuggestions(nextSuggestions);
        setSuggestionsOpen(nextSuggestions.length > 0);
        setHighlightedIndex(nextSuggestions.length > 0 ? 0 : -1);
      } catch (error) {
        if (controller.signal.aborted || abortRef.current !== controller || (error instanceof DOMException && error.name === "AbortError")) {
          return;
        }

        setSuggestions([]);
        setSuggestionsOpen(false);
        setHighlightedIndex(-1);
      } finally {
        if (abortRef.current === controller) {
          abortRef.current = null;
          setIsLoadingSuggestions(false);
        }
      }
    }, normalizedDraft ? 140 : 0);

    return () => {
      window.clearTimeout(timeoutId);
      controller.abort();
      if (abortRef.current === controller) abortRef.current = null;
    };
  }, [draft, tokens, isFocused, suggestionsDismissed]);

  const showSuggestionPanel = isFocused && !suggestionsDismissed && (isLoadingSuggestions || suggestionsOpen);
  const individualSendControl =
    name === "to" ? (
      <div className="flex h-10 w-full shrink-0 items-center gap-1.5 sm:w-auto">
        <label className="flex cursor-pointer items-center gap-1.5 whitespace-nowrap text-[12px] font-medium text-[#50617b]">
          <input
            type="checkbox"
            name="sendIndividually"
            value="1"
            checked={sendIndividually}
            onChange={(event) => onSendIndividuallyChange?.(event.target.checked)}
            className="h-4 w-4 rounded-[4px] border-[#cfd7e4] text-[#2f6df6] focus:ring-2 focus:ring-[#d7e5ff] focus:ring-offset-0"
          />
          <span>{t("compose.recipient.individual")}</span>
        </label>
        <span className="group relative inline-flex">
          <button
            type="button"
            aria-label={`${t("compose.recipient.individual")} 도움말`}
            aria-describedby={`${inputId}-individual-help`}
            aria-expanded={individualHelpOpen}
            onClick={() => setIndividualHelpOpen((current) => !current)}
            onBlur={() => setIndividualHelpOpen(false)}
            className="inline-flex h-4 w-4 cursor-help items-center justify-center rounded-[4px] border border-[#b9c2d1] bg-white text-[10px] font-bold leading-none text-[#7d899b] transition-colors hover:border-[#8ca7d5] hover:text-[#2f6df6] focus-visible:border-[#2f6df6] focus-visible:text-[#2f6df6] focus-visible:outline-none"
          >
            ?
          </button>
          <span
            id={`${inputId}-individual-help`}
            role="tooltip"
            className={`absolute left-1/2 top-[calc(100%+9px)] z-40 w-[270px] -translate-x-1/2 rounded-[7px] border border-[#cfd9ea] bg-white px-3.5 py-3 text-left text-[12px] font-normal leading-[1.55] whitespace-normal text-[#253652] shadow-[0_8px_24px_rgba(29,48,82,0.12)] transition-opacity before:absolute before:-top-[5px] before:left-1/2 before:h-2.5 before:w-2.5 before:-translate-x-1/2 before:rotate-45 before:border-l before:border-t before:border-[#cfd9ea] before:bg-white sm:left-0 sm:translate-x-0 sm:before:left-5 ${
              individualHelpOpen
                ? "visible opacity-100"
                : "pointer-events-none invisible opacity-0 group-hover:visible group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:visible group-focus-within:pointer-events-auto group-focus-within:opacity-100"
            }`}
          >
            {t("compose.recipient.individualHelp")}
          </span>
        </span>
      </div>
    ) : null;

  return (
    <div className="grid grid-cols-1 items-start gap-1.5 sm:grid-cols-[88px_minmax(0,1fr)] sm:gap-3">
      <label htmlFor={inputId} className="text-[13px] font-medium text-[#6f809d] sm:pt-2">
        {label}
      </label>
      <div className="relative">
        <div className="flex flex-wrap items-start gap-2 sm:flex-nowrap">
          {individualSendControl}
          <div
            onClick={() => {
              setSuggestionsDismissed(false);
              inputRef.current?.focus();
            }}
            className={`flex min-h-10 min-w-0 flex-1 flex-wrap items-center gap-1.5 rounded-[10px] border px-3 py-0.5 transition-colors ${
              errorMessage
                ? "border-[#e45b5b] bg-[#fffafa] shadow-[0_0_0_3px_rgba(228,91,91,0.08)] focus-within:border-[#d94848] focus-within:bg-white"
                : "border-[#e2eaf8] bg-[#fbfcff] hover:border-[#cfdbf4] focus-within:border-[#cbdcff] focus-within:bg-white"
            }`}
          >
          {tokens.map((token, index) => (
            <div
              key={token.email}
              className="group flex h-8 items-center rounded-[10px] border border-[#d8e4fb] bg-white px-2.5 text-[12px] text-[#28405f] shadow-[0_8px_18px_rgba(25,38,72,0.05)] transition-all duration-200 hover:-translate-y-px hover:border-[#bdd1ff] hover:text-[#2f6df6] hover:shadow-[0_14px_24px_rgba(47,109,246,0.1)]"
            >
              <button
                type="button"
                onClick={() => editToken(index)}
                className="max-w-[320px] cursor-pointer truncate pr-2 text-left"
              >
                {token.label}
              </button>
              <button
                type="button"
                aria-label={`${token.label} ${t("compose.file.remove")}`}
                onClick={(event) => {
                  event.stopPropagation();
                  removeToken(index);
                }}
                className="inline-flex h-5 w-5 cursor-pointer items-center justify-center rounded-[10px] text-[#98a7be] transition-colors hover:bg-[#eef4ff] hover:text-[#2f6df6]"
              >
                ×
              </button>
            </div>
          ))}

          <input
            id={inputId}
            ref={inputRef}
            type="text"
            autoFocus={autoFocus}
            autoComplete="off"
            value={draft}
            placeholder={tokens.length === 0 ? placeholder : ""}
            role="combobox"
            aria-autocomplete="list"
            aria-controls={showSuggestionPanel ? suggestionListId : undefined}
            aria-describedby={errorMessage ? errorMessageId : undefined}
            aria-expanded={showSuggestionPanel}
            aria-activedescendant={showSuggestionPanel && highlightedIndex >= 0 ? `${suggestionListId}-${highlightedIndex}` : undefined}
            aria-invalid={Boolean(errorMessage)}
            onBlur={() => {
              abortRef.current?.abort();
              abortRef.current = null;
              setIsFocused(false);
              commitDraft();
            }}
            onFocus={() => {
              setIsFocused(true);
              setSuggestionsDismissed(false);
              if (suggestions.length > 0) {
                setSuggestionsOpen(true);
              }
            }}
            onChange={(event) => {
              const nextValue = event.target.value;
              setSuggestionsDismissed(false);
              onInteract?.();

              if (!nextValue.trim()) {
                abortRef.current?.abort();
                abortRef.current = null;
                setDraft(nextValue);
                setIsLoadingSuggestions(false);
                setSuggestions([]);
                setSuggestionsOpen(false);
                setHighlightedIndex(-1);
                return;
              }

              if (hasRecipientSeparator(nextValue)) {
                setTokens((current) => mergeRecipientTokens(current, splitRecipientTokens(nextValue)));
                setDraft("");
                setIsLoadingSuggestions(false);
                setSuggestions([]);
                setSuggestionsOpen(false);
                setHighlightedIndex(-1);
                return;
              }

              setDraft(nextValue);
            }}
            onPaste={(event) => {
              const pastedText = event.clipboardData.getData("text/plain");

              if (!hasRecipientSeparator(pastedText)) {
                return;
              }

              event.preventDefault();
              onInteract?.();

              const input = event.currentTarget;
              const selectionStart = input.selectionStart ?? draft.length;
              const selectionEnd = input.selectionEnd ?? selectionStart;
              const nextValue = `${draft.slice(0, selectionStart)}${pastedText}${draft.slice(selectionEnd)}`;

              setTokens((current) =>
                mergeRecipientTokens(current, splitRecipientTokens(nextValue)),
              );
              setDraft("");
              setIsLoadingSuggestions(false);
              setSuggestions([]);
              setSuggestionsOpen(false);
              setHighlightedIndex(-1);
            }}
            onKeyDown={(event) => {
              if ((event.key === "ArrowDown" || event.key === "ArrowUp") && suggestions.length > 0) {
                event.preventDefault();
                setSuggestionsOpen(true);
                setSuggestionsDismissed(false);
                setHighlightedIndex((current) => {
                  if (event.key === "ArrowDown") {
                    return current < suggestions.length - 1 ? current + 1 : 0;
                  }

                  return current > 0 ? current - 1 : suggestions.length - 1;
                });
                return;
              }

              if (event.key === "Escape" && showSuggestionPanel) {
                event.preventDefault();
                abortRef.current?.abort();
                setIsLoadingSuggestions(false);
                setSuggestionsOpen(false);
                setSuggestionsDismissed(true);
                setHighlightedIndex(-1);
                return;
              }

              if (event.key === "Enter") {
                event.preventDefault();

                if (suggestionsOpen && highlightedIndex >= 0) {
                  const suggestion = suggestions[highlightedIndex];

                  if (suggestion) {
                    selectSuggestion(suggestion);
                    return;
                  }
                }

                commitDraft();
                return;
              }

              if (event.key === "Backspace" && !draft && tokens.length > 0) {
                event.preventDefault();
                const lastToken = tokens[tokens.length - 1];
                setTokens((current) => current.slice(0, -1));
                setDraft(lastToken?.email.slice(0, -1) ?? "");
                focusInput();
              }
            }}
            className="h-8 min-w-[160px] flex-1 rounded-[10px] border-0 bg-transparent px-1 text-[13px] text-[#1e2a43] outline-none placeholder:text-[#a0adc2]"
          />
            <input type="hidden" name={name} value={serializeRecipientEmails(tokens, draft)} />
          </div>
          {endAction ? <div className="shrink-0">{endAction}</div> : null}
        </div>
        {errorMessage ? (
          <p id={errorMessageId} role="alert" className="mt-1.5 text-[12px] font-medium text-[#d24747]">
            {errorMessage}
          </p>
        ) : null}
        {showSuggestionPanel ? (
          <div
            id={suggestionListId}
            role="listbox"
            className="absolute left-0 right-0 top-[calc(100%+8px)] z-20 max-h-[min(360px,45vh)] overflow-y-auto overscroll-contain rounded-[14px] border border-[#dfe8f7] bg-[rgba(255,255,255,0.98)] p-1.5 shadow-[0_20px_40px_rgba(20,34,64,0.12)] backdrop-blur-[18px]"
          >
            {isLoadingSuggestions ? (
              <div className="px-3 py-2 text-[12px] text-[#72839f]">{t("compose.recipient.searching")}</div>
            ) : (
              suggestions.map((suggestion, index) => {
                const isActive = index === highlightedIndex;

                return (
                  <button
                    key={`${suggestion.kind}-${suggestion.email}`}
                    id={`${suggestionListId}-${index}`}
                    type="button"
                    role="option"
                    aria-selected={isActive}
                    onMouseDown={(event) => {
                      event.preventDefault();
                    }}
                    onClick={() => selectSuggestion(suggestion)}
                    className={[
                      "flex w-full cursor-pointer items-center justify-between gap-3 rounded-[12px] px-3 py-2 text-left transition-colors",
                      isActive
                        ? "bg-[#eef4ff] text-[#20478f]"
                        : "text-[#31435f] hover:bg-[#f7faff] hover:text-[#2f6df6]",
                    ].join(" ")}
                  >
                    <p className="min-w-0 truncate text-[13px] font-medium">{suggestion.label}</p>
                    <span className="shrink-0 text-[11px] font-semibold text-[#8a99b2]">
                      {suggestion.kind === "member"
                        ? t("compose.recipient.member")
                        : t("compose.recipient.recent")}
                    </span>
                  </button>
                );
              })
            )}
          </div>
        ) : null}
      </div>
    </div>
  );
  },
);

export function MailComposeForm({
  growth,
  mailboxAddress,
  composeMode,
  sourceDraftMessageId,
  draftAttachments,
  currentFolder,
  defaultBcc,
  defaultBody,
  defaultBodyHtml,
  defaultCc,
  defaultSubject,
  defaultTo,
  signatureHtml,
  signatureText,
}: MailComposeFormProps) {
  const { t } = useI18n();
  const router = useRouter();
  const toolbarId = useId().replace(/:/g, "");
  const formRef = useRef<HTMLFormElement | null>(null);
  const attachmentInputRef = useRef<HTMLInputElement | null>(null);
  const imageInputRef = useRef<HTMLInputElement | null>(null);
  const editorShellRef = useRef<HTMLDivElement | null>(null);
  const preservedHtmlEditorRef = useRef<HTMLIFrameElement | null>(null);
  const preservedHtmlEditorCleanupRef = useRef<(() => void) | null>(null);
  const preservedHtmlSelectionRef = useRef<Range | null>(null);
  const activeHtmlLinkRef = useRef<HTMLAnchorElement | null>(null);
  const htmlSourceEditorRef = useRef<HTMLTextAreaElement | null>(null);
  const subjectInputRef = useRef<HTMLInputElement | null>(null);
  const toRecipientFieldRef = useRef<RecipientFieldHandle | null>(null);
  const ccRecipientFieldRef = useRef<RecipientFieldHandle | null>(null);
  const bccRecipientFieldRef = useRef<RecipientFieldHandle | null>(null);
  const shouldFocusEditor =
    composeMode === "reply" || composeMode === "replyAll" || composeMode === "forward";
  const normalizedDefaultBody = insertComposeSignatureText({
    bodyText: defaultBody,
    composeMode,
    signatureText,
  });
  const normalizedDefaultBodyHtml = insertComposeSignatureHtml({
    bodyHtml: defaultBodyHtml,
    composeMode,
    signatureHtml,
  });
  const initialBodyText = shouldFocusEditor
    ? ensureLeadingComposeSpaceText(normalizedDefaultBody)
    : normalizedDefaultBody;
  const initialBodyHtml = shouldFocusEditor
    ? ensureLeadingComposeSpaceHtml(normalizedDefaultBodyHtml)
    : normalizedDefaultBodyHtml;
  const initialEditorHtml =
    initialBodyHtml || (initialBodyText.trim() ? plainTextToHtml(initialBodyText) : "<p><br></p>");
  const [attachmentFiles, setAttachmentFiles] = useState<ComposeLocalAttachment[]>([]);
  const retainedDraftAttachments = composeMode === "draft"
    ? draftAttachments.filter((attachment) => attachment.contentDisposition === "attachment" && !attachment.isInline)
    : [];
  const regularAttachmentBytes = retainedDraftAttachments
    .filter((attachment) => !attachment.isLargeAttachment)
    .reduce((total, attachment) => total + attachment.sizeBytes, 0) + attachmentFiles
    .filter((attachment) => attachment.mode === "regular")
    .reduce((total, attachment) => total + attachment.sizeBytes, 0);
  const largeAttachmentBytes = retainedDraftAttachments
    .filter((attachment) => attachment.isLargeAttachment)
    .reduce((total, attachment) => total + attachment.sizeBytes, 0) + attachmentFiles
    .filter((attachment) => attachment.mode !== "regular")
    .reduce((total, attachment) => total + attachment.sizeBytes, 0);
  const [attachmentDragActive, setAttachmentDragActive] = useState(false);
  const [htmlEditorViewportSize, setHtmlEditorViewportSize] =
    useState<HtmlEditorViewportSize>("large");
  const [htmlEditorViewMode, setHtmlEditorViewMode] =
    useState<HtmlEditorViewMode>("visual");
  const [htmlLinkDialog, setHtmlLinkDialog] = useState<HtmlLinkDialogState | null>(null);
  const [htmlLinkError, setHtmlLinkError] = useState<string | null>(null);
  const [htmlLinkPopover, setHtmlLinkPopover] = useState<HtmlLinkPopoverState | null>(null);
  const [inlineImageUploads, setInlineImageUploads] = useState<ComposeUploadItem[]>([]);
  const [subject, setSubject] = useState(defaultSubject);
  const [sendIndividually, setSendIndividually] = useState(false);
  const [bodyText, setBodyText] = useState(initialBodyText);
  const [bodyHtml, setBodyHtml] = useState(initialEditorHtml);
  const [htmlSource, setHtmlSource] = useState(() =>
    getPreservedHtmlEditorSource(initialEditorHtml),
  );
  const [fontFamily, setFontFamily] = useState("Pretendard");
  const [fontSize, setFontSize] = useState("15px");
  const [backgroundColor, setBackgroundColor] = useState(defaultComposeBackgroundColor);
  const [textColor, setTextColor] = useState(defaultComposeTextColor);
  const [uploadProgress, setUploadProgress] = useState<ComposeUploadProgressState | null>(null);
  const [uploadError, setUploadError] = useState<string | null>(null);
  const [uploadingCount, setUploadingCount] = useState(0);
  const [sendModalState, setSendModalState] = useState<ComposeSendModalState | null>(null);
  const [submitIntent, setSubmitIntent] = useState<"send" | "draft">("send");
  const [validationErrors, setValidationErrors] = useState<ComposeValidationErrors>({});
  const [recipientFocusSignal, setRecipientFocusSignal] = useState(0);
  const [addressBookOpen, setAddressBookOpen] = useState(false);
  const [addressBookInitialSelection, setAddressBookInitialSelection] =
    useState<ComposeAddressBookSelection>(() => ({
      bcc: splitRecipientTokens(defaultBcc),
      cc: splitRecipientTokens(defaultCc),
      to: splitRecipientTokens(defaultTo),
    }));
  const activeUploadProgressSessionRef = useRef(0);
  const uploadProgressHideTimerRef = useRef<number | null>(null);
  const sendRedirectTimerRef = useRef<number | null>(null);
  const sendRequestLockedRef = useRef(false);
  const composeTitle = buildComposeTitle(composeMode, t);
  const isUploading = uploadingCount > 0;
  const isSendProcessing = sendModalState !== null && sendModalState.stage !== "error";
  const submitLocked = isUploading || isSendProcessing;
  const uploadFilesRef = useRef<(kind: ComposeUploadKind, files: File[]) => Promise<ComposeUploadItem[]>>(
    async () => [],
  );
  const handleAttachmentFilesRef = useRef<(files: File[]) => Promise<void>>(async () => undefined);
  const handleInlineImageFilesRef = useRef<(files: File[]) => Promise<void>>(async () => undefined);

  const clearValidationError = (field: keyof ComposeValidationErrors) => {
    setValidationErrors((current) =>
      current[field]
        ? {
            ...current,
            [field]: undefined,
          }
        : current,
    );
  };

  const openAddressBook = () => {
    setAddressBookInitialSelection({
      bcc: bccRecipientFieldRef.current?.getRecipients() ?? [],
      cc: ccRecipientFieldRef.current?.getRecipients() ?? [],
      to: toRecipientFieldRef.current?.getRecipients() ?? [],
    });
    setAddressBookOpen(true);
  };

  const applyAddressBookSelection = (selection: ComposeAddressBookSelection) => {
    toRecipientFieldRef.current?.replaceRecipients(selection.to);
    ccRecipientFieldRef.current?.replaceRecipients(selection.cc);
    bccRecipientFieldRef.current?.replaceRecipients(selection.bcc);
    clearValidationError("recipient");
    setAddressBookOpen(false);
  };

  const getPreservedHtmlEditorBody = () =>
    preservedHtmlEditorRef.current?.contentDocument?.body ?? null;

  const capturePreservedHtmlSelection = () => {
    const frameDocument = preservedHtmlEditorRef.current?.contentDocument;
    const editorBody = frameDocument?.body;
    const selection = frameDocument?.getSelection();

    if (!editorBody || !selection || selection.rangeCount === 0) {
      return null;
    }

    const range = selection.getRangeAt(0);

    if (!editorBody.contains(range.commonAncestorContainer)) {
      return null;
    }

    const nextRange = range.cloneRange();
    preservedHtmlSelectionRef.current = nextRange;
    return nextRange;
  };

  const restorePreservedHtmlSelection = () => {
    const frameDocument = preservedHtmlEditorRef.current?.contentDocument;
    const editorBody = frameDocument?.body;
    const selection = frameDocument?.getSelection();
    const savedRange = preservedHtmlSelectionRef.current;

    if (!editorBody || !selection || !savedRange) {
      return null;
    }

    if (!editorBody.contains(savedRange.commonAncestorContainer)) {
      preservedHtmlSelectionRef.current = null;
      return null;
    }

    selection.removeAllRanges();
    selection.addRange(savedRange);
    return savedRange;
  };

  const getHtmlLinkFromSelection = () => {
    const frameDocument = preservedHtmlEditorRef.current?.contentDocument;
    const editorBody = frameDocument?.body;
    const selection = frameDocument?.getSelection();
    const anchorNode = selection?.anchorNode ?? null;

    if (!editorBody || !anchorNode || !editorBody.contains(anchorNode)) {
      return null;
    }

    const anchorElement =
      anchorNode.nodeType === Node.ELEMENT_NODE
        ? (anchorNode as Element).closest("a")
        : anchorNode.parentElement?.closest("a");

    return anchorElement?.tagName === "A" ? (anchorElement as HTMLAnchorElement) : null;
  };

  const hideHtmlLinkPopover = () => {
    activeHtmlLinkRef.current = null;
    setHtmlLinkPopover(null);
  };

  const showHtmlLinkPopover = (anchor: HTMLAnchorElement) => {
    const frame = preservedHtmlEditorRef.current;
    const editorShell = editorShellRef.current;

    if (!frame || !editorShell || !anchor.isConnected) {
      hideHtmlLinkPopover();
      return;
    }

    anchor.scrollIntoView({ block: "nearest", inline: "nearest" });
    const frameRect = frame.getBoundingClientRect();
    const shellRect = editorShell.getBoundingClientRect();
    const anchorRect = anchor.getBoundingClientRect();
    const popoverHalfWidth = Math.min(190, Math.max(130, editorShell.clientWidth / 2 - 12));
    const preferredLeft = frameRect.left - shellRect.left + anchorRect.left + anchorRect.width / 2;
    const left = Math.min(
      Math.max(popoverHalfWidth, preferredLeft),
      Math.max(popoverHalfWidth, editorShell.clientWidth - popoverHalfWidth),
    );

    activeHtmlLinkRef.current = anchor;
    setHtmlLinkPopover({
      href: anchor.getAttribute("href") ?? anchor.href,
      left,
      top:
        frameRect.top -
        shellRect.top +
        Math.min(frameRect.height, Math.max(0, anchorRect.bottom)) +
        7,
    });
  };

  const syncPreservedHtmlEditor = () => {
    const editorBody = getPreservedHtmlEditorBody();

    if (!editorBody) {
      return;
    }

    const nextHtml = editorBody.innerHTML;
    setBodyHtml(nextHtml);
    setHtmlSource(getPreservedHtmlEditorSource(nextHtml));
    setBodyText(normalizeEditorText(editorBody.innerText));
    clearValidationError("body");
  };

  const insertHtmlNodesAtSelection = (nodes: Node[]) => {
    const frameDocument = preservedHtmlEditorRef.current?.contentDocument;
    const editorBody = frameDocument?.body;
    const selection = frameDocument?.getSelection();

    if (!frameDocument || !editorBody || !selection || nodes.length === 0) {
      return null;
    }

    editorBody.focus({ preventScroll: true });
    let range = restorePreservedHtmlSelection();

    if (!range) {
      range = frameDocument.createRange();
      range.selectNodeContents(editorBody);
      range.collapse(false);
    }

    range.deleteContents();

    nodes.forEach((node) => {
      range?.insertNode(node);
      range?.setStartAfter(node);
      range?.collapse(true);
    });

    selection.removeAllRanges();
    selection.addRange(range);
    preservedHtmlSelectionRef.current = range.cloneRange();
    syncPreservedHtmlEditor();
    return range;
  };

  const openHtmlLinkDialog = (anchor?: HTMLAnchorElement | null) => {
    if (!anchor) {
      const selection = preservedHtmlEditorRef.current?.contentDocument?.getSelection();
      capturePreservedHtmlSelection();
      activeHtmlLinkRef.current = null;
      setHtmlLinkDialog({
        href: "",
        mode: "create",
        text: selection?.toString().trim() ?? "",
      });
    } else {
      activeHtmlLinkRef.current = anchor;
      setHtmlLinkDialog({
        href: anchor.getAttribute("href") ?? "",
        mode: "edit",
        text: anchor.textContent ?? "",
      });
    }

    setHtmlLinkError(null);
    setHtmlLinkPopover(null);
  };

  const saveHtmlLink = () => {
    if (!htmlLinkDialog) {
      return;
    }

    const text = htmlLinkDialog.text.trim();
    const href = normalizeComposeLinkHref(htmlLinkDialog.href);

    if (!text || !href) {
      setHtmlLinkError(t("compose.editor.link.invalid"));
      return;
    }

    const frameDocument = preservedHtmlEditorRef.current?.contentDocument;
    const editorBody = frameDocument?.body;

    if (!frameDocument || !editorBody) {
      return;
    }

    let anchor = activeHtmlLinkRef.current;

    if (htmlLinkDialog.mode === "edit" && anchor?.isConnected) {
      anchor.textContent = text;
      anchor.href = href;
      anchor.target = "_blank";
      anchor.rel = "noopener noreferrer";
      syncPreservedHtmlEditor();
    } else {
      anchor = frameDocument.createElement("a");
      anchor.textContent = text;
      anchor.href = href;
      anchor.target = "_blank";
      anchor.rel = "noopener noreferrer";
      insertHtmlNodesAtSelection([anchor]);

      const range = frameDocument.createRange();
      range.selectNodeContents(anchor);
      range.collapse(false);
      const selection = frameDocument.getSelection();
      selection?.removeAllRanges();
      selection?.addRange(range);
      preservedHtmlSelectionRef.current = range.cloneRange();
    }

    setHtmlLinkDialog(null);
    setHtmlLinkError(null);

    window.requestAnimationFrame(() => {
      if (anchor?.isConnected) {
        showHtmlLinkPopover(anchor);
      }
    });
  };

  const removeActiveHtmlLink = () => {
    const anchor = activeHtmlLinkRef.current;
    const frameDocument = preservedHtmlEditorRef.current?.contentDocument;

    if (!anchor?.isConnected || !frameDocument) {
      hideHtmlLinkPopover();
      return;
    }

    const parent = anchor.parentNode;

    if (!parent) {
      hideHtmlLinkPopover();
      return;
    }

    const movedNodes = Array.from(anchor.childNodes);
    movedNodes.forEach((node) => parent.insertBefore(node, anchor));
    anchor.remove();

    const range = frameDocument.createRange();
    const lastMovedNode = movedNodes.at(-1);
    if (lastMovedNode) {
      range.setStartAfter(lastMovedNode);
    } else {
      range.selectNodeContents(parent);
      range.collapse(false);
    }
    range.collapse(true);
    preservedHtmlSelectionRef.current = range;
    hideHtmlLinkPopover();
    syncPreservedHtmlEditor();
  };

  const connectPreservedHtmlEditor = useEffectEvent((frame: HTMLIFrameElement) => {
    preservedHtmlEditorCleanupRef.current?.();
    preservedHtmlEditorCleanupRef.current = null;

    const frameDocument = frame.contentDocument;

    if (!frameDocument?.body) {
      return;
    }

    const editorBody = frameDocument.body;
    editorBody.contentEditable = "true";
    editorBody.spellcheck = true;
    editorBody.setAttribute("role", "textbox");
    editorBody.setAttribute("aria-label", t("compose.editor.html.body"));

    const preparedHtml = prepareEditableEmailHtml(bodyHtml);
    editorBody.innerHTML = preparedHtml?.html ?? bodyHtml;

    const syncSelectionAndLink = () => {
      capturePreservedHtmlSelection();
      const selectedLink = getHtmlLinkFromSelection();

      if (selectedLink) {
        showHtmlLinkPopover(selectedLink);
      }
    };
    const handleInput = () => {
      syncPreservedHtmlEditor();
      capturePreservedHtmlSelection();

      if (activeHtmlLinkRef.current?.isConnected) {
        showHtmlLinkPopover(activeHtmlLinkRef.current);
      }
    };
    const handleClick = (event: MouseEvent) => {
      const target = event.target as Element | null;
      const anchor = target?.closest?.("a");

      if (anchor?.tagName === "A") {
        event.preventDefault();
        capturePreservedHtmlSelection();
        showHtmlLinkPopover(anchor as HTMLAnchorElement);
      } else if (!htmlLinkDialog) {
        hideHtmlLinkPopover();
      }
    };
    const handlePaste = (event: ClipboardEvent) => {
      const files = Array.from(event.clipboardData?.files ?? []).filter((file) =>
        file.type.startsWith("image/"),
      );

      if (files.length > 0) {
        event.preventDefault();
        event.stopImmediatePropagation();
        capturePreservedHtmlSelection();
        void handleInlineImageFilesRef.current(files);
        return;
      }

      const pastedHtml = event.clipboardData?.getData("text/html") ?? "";
      const preparedHtml = prepareEditableEmailHtml(pastedHtml);

      if (!pastedHtml.trim() || !preparedHtml) {
        return;
      }

      event.preventDefault();
      event.stopImmediatePropagation();
      capturePreservedHtmlSelection();
      editorBody.focus({ preventScroll: true });
      restorePreservedHtmlSelection();
      frameDocument.execCommand(
        "insertHTML",
        false,
        getPreservedHtmlEditorSource(preparedHtml.html),
      );
      syncPreservedHtmlEditor();
    };
    const handleDragOver = (event: DragEvent) => {
      if (Array.from(event.dataTransfer?.items ?? []).some((item) => item.type.startsWith("image/"))) {
        event.preventDefault();
      }
    };
    const handleDrop = (event: DragEvent) => {
      const files = Array.from(event.dataTransfer?.files ?? []).filter((file) =>
        file.type.startsWith("image/"),
      );

      if (files.length === 0) {
        return;
      }

      event.preventDefault();
      const caretDocument = frameDocument as Document & {
        caretRangeFromPoint?: (x: number, y: number) => Range | null;
      };
      const dropRange = caretDocument.caretRangeFromPoint?.(event.clientX, event.clientY) ?? null;

      if (dropRange && editorBody.contains(dropRange.commonAncestorContainer)) {
        preservedHtmlSelectionRef.current = dropRange.cloneRange();
      } else {
        capturePreservedHtmlSelection();
      }

      void handleInlineImageFilesRef.current(files);
    };
    const hideLinkTools = () => {
      if (!htmlLinkDialog) {
        hideHtmlLinkPopover();
      }
    };

    frameDocument.addEventListener("input", handleInput);
    frameDocument.addEventListener("click", handleClick);
    frameDocument.addEventListener("selectionchange", syncSelectionAndLink);
    editorBody.addEventListener("keyup", syncSelectionAndLink);
    editorBody.addEventListener("mouseup", syncSelectionAndLink);
    editorBody.addEventListener("paste", handlePaste, true);
    editorBody.addEventListener("dragover", handleDragOver, true);
    editorBody.addEventListener("drop", handleDrop, true);
    frame.contentWindow?.addEventListener("scroll", hideLinkTools);
    window.addEventListener("resize", hideLinkTools);

    if (shouldFocusEditor) {
      window.requestAnimationFrame(() => {
        editorBody.focus({ preventScroll: true });
        const range = frameDocument.createRange();
        range.selectNodeContents(editorBody);
        range.collapse(true);
        const selection = frameDocument.getSelection();
        selection?.removeAllRanges();
        selection?.addRange(range);
        preservedHtmlSelectionRef.current = range.cloneRange();
      });
    }

    preservedHtmlEditorCleanupRef.current = () => {
      frameDocument.removeEventListener("input", handleInput);
      frameDocument.removeEventListener("click", handleClick);
      frameDocument.removeEventListener("selectionchange", syncSelectionAndLink);
      editorBody.removeEventListener("keyup", syncSelectionAndLink);
      editorBody.removeEventListener("mouseup", syncSelectionAndLink);
      editorBody.removeEventListener("paste", handlePaste, true);
      editorBody.removeEventListener("dragover", handleDragOver, true);
      editorBody.removeEventListener("drop", handleDrop, true);
      frame.contentWindow?.removeEventListener("scroll", hideLinkTools);
      window.removeEventListener("resize", hideLinkTools);
    };
  });

  const applyPreservedHtmlCommand = (command: string, value?: string) => {
    const frameDocument = preservedHtmlEditorRef.current?.contentDocument;

    if (!frameDocument?.body) {
      return;
    }

    hideHtmlLinkPopover();
    frameDocument.body.focus({ preventScroll: true });
    restorePreservedHtmlSelection();
    frameDocument.execCommand("styleWithCSS", false, "true");
    frameDocument.execCommand(command, false, value);
    capturePreservedHtmlSelection();
    syncPreservedHtmlEditor();
  };

  const applyPreservedHtmlFontSize = (value: string) => {
    const frameDocument = preservedHtmlEditorRef.current?.contentDocument;

    if (!frameDocument?.body) {
      return;
    }

    const legacySize =
      ({ "13px": "2", "15px": "3", "17px": "3", "19px": "4", "24px": "5", "32px": "6" } as Record<string, string>)[value] ?? "3";
    const existingCandidates = new Set(
      frameDocument.querySelectorAll<HTMLElement>('font[size], [style*="font-size"]'),
    );
    frameDocument.body.focus({ preventScroll: true });
    restorePreservedHtmlSelection();
    frameDocument.execCommand("styleWithCSS", false, "true");
    frameDocument.execCommand("fontSize", false, legacySize);

    const selection = frameDocument.getSelection();
    const range = selection?.rangeCount ? selection.getRangeAt(0) : null;
    frameDocument
      .querySelectorAll<HTMLElement>('font[size], [style*="font-size"]')
      .forEach((element) => {
        if (existingCandidates.has(element)) {
          return;
        }

        try {
          if (!range || range.collapsed || range.intersectsNode(element)) {
            element.removeAttribute("size");
            element.style.fontSize = value;
          }
        } catch {
          // Ignore detached formatting nodes created by the browser command.
        }
      });

    capturePreservedHtmlSelection();
    syncPreservedHtmlEditor();
  };

  useEffect(() => {
    const frame = preservedHtmlEditorRef.current;

    if (htmlEditorViewMode !== "visual" || !frame) {
      return;
    }

    const handleLoad = () => connectPreservedHtmlEditor(frame);
    frame.addEventListener("load", handleLoad);

    if (frame.contentDocument?.readyState === "complete") {
      handleLoad();
    }

    return () => {
      frame.removeEventListener("load", handleLoad);
      preservedHtmlEditorCleanupRef.current?.();
      preservedHtmlEditorCleanupRef.current = null;
    };
  }, [htmlEditorViewMode]);

  useLayoutEffect(() => {
    const formElement = formRef.current;

    if (!formElement) {
      return undefined;
    }

    const scrollComposeViewToTop = () => {
      formElement.scrollTop = 0;

      let parent: HTMLElement | null = formElement.parentElement;

      while (parent) {
        const style = window.getComputedStyle(parent);
        const isScrollable =
          /(auto|scroll)/.test(style.overflowY) && parent.scrollHeight > parent.clientHeight;

        if (isScrollable) {
          parent.scrollTop = 0;
        }

        parent = parent.parentElement;
      }

      window.scrollTo(0, 0);
      document.documentElement.scrollTop = 0;
      document.body.scrollTop = 0;
    };

    scrollComposeViewToTop();
    const frameId = window.requestAnimationFrame(scrollComposeViewToTop);

    return () => {
      window.cancelAnimationFrame(frameId);
    };
  }, []);

  useEffect(() => {
    return () => {
      if (uploadProgressHideTimerRef.current !== null) {
        window.clearTimeout(uploadProgressHideTimerRef.current);
      }

      if (sendRedirectTimerRef.current !== null) {
        window.clearTimeout(sendRedirectTimerRef.current);
      }
    };
  }, []);

  useEffect(() => {
    uploadFilesRef.current = async (kind: ComposeUploadKind, files: File[]) => {
      if (files.length === 0) {
        return [];
      }

      setUploadError(null);
      const uploadSessionId = activeUploadProgressSessionRef.current + 1;
      activeUploadProgressSessionRef.current = uploadSessionId;

      if (uploadProgressHideTimerRef.current !== null) {
        window.clearTimeout(uploadProgressHideTimerRef.current);
        uploadProgressHideTimerRef.current = null;
      }

      setUploadProgress(
        buildComposeUploadProgressState(kind, files, 0, t("compose.upload.preparing")),
      );

      try {
        const formData = new FormData();
        formData.set("kind", kind);
        files.forEach((file) => {
          formData.append("files", file);
        });

        const uploaded = await new Promise<ComposeUploadItem[]>((resolve, reject) => {
          const xhr = new XMLHttpRequest();
          xhr.open("POST", "/api/mailbox/uploads");
          xhr.responseType = "json";
          xhr.upload.addEventListener("progress", (event) => {
            const progressRatio =
              event.lengthComputable && event.total > 0 ? event.loaded / event.total : 0;
            if (activeUploadProgressSessionRef.current !== uploadSessionId) {
              return;
            }

            setUploadProgress(
              buildComposeUploadProgressState(
                kind,
                files,
                progressRatio,
                t("compose.upload.preparing"),
              ),
            );
          });

          xhr.addEventListener("load", () => {
            const payload = parseComposeUploadResponse(xhr);

            if (xhr.status < 200 || xhr.status >= 300 || !payload?.uploads) {
              reject(new Error(payload?.error || t("compose.upload.failed")));
              return;
            }

            resolve(payload.uploads);
          });

          xhr.addEventListener("error", () => {
            reject(new Error(t("compose.upload.failed")));
          });

          xhr.addEventListener("abort", () => {
            reject(new Error(t("compose.upload.interrupted")));
          });

          xhr.send(formData);
        });

        if (activeUploadProgressSessionRef.current === uploadSessionId) {
          setUploadProgress(
            buildComposeUploadProgressState(kind, files, 1, t("compose.upload.preparing")),
          );
        }

        return uploaded;
      } catch (error) {
        const message = error instanceof Error ? error.message : t("compose.upload.failed");
        setUploadError(message);
        throw error;
      } finally {
        uploadProgressHideTimerRef.current = window.setTimeout(() => {
          if (activeUploadProgressSessionRef.current === uploadSessionId) {
            setUploadProgress(null);
          }
        }, 420);
      }
    };

    handleAttachmentFilesRef.current = async (files: File[]) => {
      if (files.length === 0) {
        return;
      }

      setUploadError(null);
      const incoming: ComposeLocalAttachment[] = [];
      let regularTotalBytes = regularAttachmentBytes;
      for (const file of files) {
        try {
          const mode = initialAttachmentMode({
            fileSizeBytes: file.size,
            regularTotalBytes,
            growth,
          });
          incoming.push(toLocalAttachment(file, mode));
          if (mode === "regular") regularTotalBytes += file.size;
        } catch (error) {
          setUploadError(error instanceof Error && error.message === "attachment-large-limit"
            ? `${file.name}: 대용량 첨부는 파일당 최대 250MB입니다.`
            : `${file.name}: 무료 플랜은 일반 첨부 총합 25MB까지 가능합니다.`);
        }
      }
      setAttachmentFiles((current) => mergeLocalAttachments(current, incoming));
      setValidationErrors((current) =>
        current.body ? { ...current, body: undefined } : current,
      );
    };

    handleInlineImageFilesRef.current = async (files: File[]) => {
      const imageFiles = files.filter((file) => file.type.startsWith("image/"));

      if (imageFiles.length === 0) {
        return;
      }

      setUploadingCount((current) => current + 1);

      try {
        const uploaded = await uploadFilesRef.current("inline-image", imageFiles);
        setInlineImageUploads((current) => mergeUploads(current, uploaded));
        setValidationErrors((current) =>
          current.body ? { ...current, body: undefined } : current,
        );

        const frameDocument = preservedHtmlEditorRef.current?.contentDocument;

        if (frameDocument?.body && htmlEditorViewMode === "visual") {
          const nodes = uploaded.flatMap((upload) => {
            const image = frameDocument.createElement("img");
            image.src = upload.previewUrl;
            image.alt = upload.filename;
            image.style.display = "block";
            image.style.height = "auto";
            image.style.maxWidth = "100%";
            image.style.margin = "12px 0";
            return [image, frameDocument.createElement("br")];
          });

          insertHtmlNodesAtSelection(nodes);
          return;
        }

        const imageHtml = uploaded
          .map(
            (upload) =>
              `<img src="${escapeHtml(upload.previewUrl)}" alt="${escapeHtml(upload.filename)}" style="display:block;height:auto;max-width:100%;margin:12px 0" /><br />`,
          )
          .join("");
        setHtmlSource((current) => `${current}${imageHtml}`);
        setBodyHtml((current) =>
          wrapPreservedHtmlEditorSource(`${getPreservedHtmlEditorSource(current)}${imageHtml}`),
        );
      } finally {
        setUploadingCount((current) => Math.max(0, current - 1));
      }
    };
  });

  useLayoutEffect(() => {
    const editorBody = getPreservedHtmlEditorBody();

    if (htmlEditorViewMode !== "visual" || !editorBody) {
      return;
    }

    const preparedHtml = prepareEditableEmailHtml(bodyHtml);
    const nextHtml = preparedHtml?.html ?? bodyHtml;

    if (
      nextHtml &&
      editorBody.ownerDocument.activeElement !== editorBody &&
      editorBody.innerHTML !== nextHtml
    ) {
      editorBody.innerHTML = nextHtml;
    }
  }, [bodyHtml, htmlEditorViewMode]);

  const effectiveBodyHtml = bodyHtml || (bodyText ? plainTextToHtml(bodyText) : "");
  const exitSave = useComposeExitSave(mailboxAddress, () => {
    if (!formRef.current) return null;
    const form = new FormData(formRef.current);
    const html = htmlEditorViewMode === "source" ? htmlSource : bodyHtml;
    const prepared = prepareEditableEmailHtml(html);
    form.set("body", prepared?.text ?? bodyText);
    form.set("bodyHtml", prepared?.html ?? html);
    form.set("subject", subject);
    for (const attachment of attachmentFiles) form.append("attachmentFiles", attachment.file, attachment.filename);
    return form;
  }, () => sendRequestLockedRef.current);

  const removeAttachmentUpload = async (upload: ComposeLocalAttachment) => {
    setAttachmentFiles((current) => current.filter((item) => item.id !== upload.id));
    setUploadError(null);
  };

  const toggleAttachmentMode = (upload: ComposeLocalAttachment) => {
    if (!growth || upload.mode === "auto_large") return;
    if (upload.mode === "user_selected_large" &&
        !canRestoreRegularAttachment({
          fileSizeBytes: upload.sizeBytes,
          regularTotalBytes: regularAttachmentBytes,
          mode: upload.mode,
          finalized: false,
        })) return;
    setAttachmentFiles((current) => current.map((item) => item.id === upload.id
      ? { ...item, mode: item.mode === "regular" ? "user_selected_large" : "regular" }
      : item));
  };

  const openAttachmentPreview = (upload: ComposeLocalAttachment) => {
    if (typeof window === "undefined") {
      return;
    }

    const previewUrl = URL.createObjectURL(upload.file);
    const popup = window.open(previewUrl, "_blank", "noopener,noreferrer");
    popup?.focus();

    window.setTimeout(() => {
      URL.revokeObjectURL(previewUrl);
    }, 60_000);
  };

  const handleSend = async (intent: "send" | "draft" = "send") => {
    if (!formRef.current || submitLocked || sendRequestLockedRef.current) {
      return;
    }

    if (sendRedirectTimerRef.current !== null) {
      window.clearTimeout(sendRedirectTimerRef.current);
      sendRedirectTimerRef.current = null;
    }

    setUploadError(null);
    const initialSendSnapshot =
      attachmentFiles.length > 0
        ? buildComposeSendSnapshotFromLocalAttachments(attachmentFiles)
        : createEmptyComposeSendProgressSnapshot();

    const requestFormData = new FormData(formRef.current);
    const firstAttachmentFilename = retainedDraftAttachments[0]?.filename ?? attachmentFiles[0]?.filename;
    const effectiveSubject = resolveComposeSubject(subject, firstAttachmentFilename);
    const preparedHtml = prepareEditableEmailHtml(
      htmlEditorViewMode === "source" ? htmlSource : bodyHtml,
    );

    if (preparedHtml) {
      requestFormData.set("body", preparedHtml.text);
      requestFormData.set("bodyHtml", preparedHtml.html);
    }

    requestFormData.set(
      "inlineImageUploadIds",
      inlineImageUploads.map((upload) => String(upload.id)).join(","),
    );
    const individualRecipientTotal = intent === "send" && sendIndividually
      ? splitRecipients(String(requestFormData.get("to") ?? "")).length
      : 0;
    const initialIndividualProgress =
      individualRecipientTotal > 1
        ? {
            completedCount: 0,
            totalCount: individualRecipientTotal,
          }
        : undefined;
    const hasRecipient = ["to", "cc", "bcc"].some((fieldName) =>
      String(requestFormData.get(fieldName) ?? "").trim(),
    );
    const hasBodyContent =
      Boolean(bodyText.trim()) || inlineImageUploads.length > 0 || attachmentFiles.length > 0 || retainedDraftAttachments.length > 0;
    const nextValidationErrors: ComposeValidationErrors = intent === "draft"
      ? {}
      : {
          body: hasBodyContent ? undefined : t("compose.error.bodyRequired"),
          recipient: hasRecipient ? undefined : t("compose.error.recipientRequired"),
          subject: effectiveSubject ? undefined : t("compose.error.subjectRequired"),
        };

    if (
      nextValidationErrors.recipient ||
      nextValidationErrors.subject ||
      nextValidationErrors.body
    ) {
      setValidationErrors(nextValidationErrors);

      if (nextValidationErrors.recipient) {
        setRecipientFocusSignal((current) => current + 1);
      } else if (nextValidationErrors.subject) {
        window.requestAnimationFrame(() => {
          subjectInputRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
          subjectInputRef.current?.focus();
        });
      } else {
        window.requestAnimationFrame(() => {
          editorShellRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });

          if (htmlEditorViewMode === "source") {
            htmlSourceEditorRef.current?.focus();
          } else {
            getPreservedHtmlEditorBody()?.focus();
          }
        });
      }

      return;
    }

    setValidationErrors({});
    setSubmitIntent(intent);
    sendRequestLockedRef.current = true;

    setSendModalState({
      individualProgress: initialIndividualProgress,
      snapshot: initialSendSnapshot,
      stage: attachmentFiles.length > 0 ? "uploading" : "sending",
    });

    const largeAttachmentUploadIds: number[] = [];
    let sendRequestStarted = false;
    try {
      const uploadedById = new Map<string, number>();
      const publishUploadProgress = () => {
        setSendModalState((current) => ({
          individualProgress: current?.individualProgress ?? initialIndividualProgress,
          snapshot: applyFileUploadProgress(initialSendSnapshot, uploadedById),
          stage: "uploading",
        }));
      };
      const largeFiles = attachmentFiles.filter((attachment) => attachment.mode !== "regular");
      const regularFiles = attachmentFiles.filter((attachment) => attachment.mode === "regular");
      for (const attachment of largeFiles) {
        const directUrl = "/api/mailbox/uploads/large/direct";
        const startResponse = await fetch(directUrl, {
          method: "POST",
          credentials: "same-origin",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            action: "start",
            contentType: attachment.contentType || "application/octet-stream",
            filename: attachment.filename,
            sizeBytes: attachment.sizeBytes,
          }),
        });
        if (!startResponse.ok) {
          throw new Error(startResponse.status === 413
            ? "mail-send-request-too-large"
            : startResponse.status === 403
              ? "mail-send-growth-required"
              : startResponse.status === 409
                ? "mail-send-drawer-quota"
                : "mail-send-service-unavailable");
        }
        const session = await startResponse.json() as {
          partBytes?: number;
          parts?: Array<{ partNumber: number; url: string }>;
          token?: string;
        };
        if (!session.token || !session.partBytes || !session.parts?.length) {
          throw new Error("mail-send-request-failed");
        }
        let completed = false;
        try {
          for (const part of session.parts) {
            const start = (part.partNumber - 1) * session.partBytes;
            const end = Math.min(start + session.partBytes, attachment.sizeBytes);
            if (start < 0 || start >= end || !part.url) throw new Error("mail-send-request-failed");
            await new Promise<void>((resolve, reject) => {
              const xhr = new XMLHttpRequest();
              xhr.open("PUT", part.url);
              xhr.upload.addEventListener("progress", (event) => {
                if (event.lengthComputable) {
                  uploadedById.set(attachment.id, Math.min(attachment.sizeBytes, start + event.loaded));
                  publishUploadProgress();
                }
              });
              xhr.addEventListener("load", () => {
                if (xhr.status >= 200 && xhr.status < 300) {
                  uploadedById.set(attachment.id, end);
                  publishUploadProgress();
                  resolve();
                } else {
                  reject(new Error("mail-send-service-unavailable"));
                }
              });
              xhr.addEventListener("error", () => reject(new Error("mail-send-network-failed")));
              xhr.addEventListener("abort", () => reject(new Error("mail-send-request-aborted")));
              xhr.send(attachment.file.slice(start, end));
            });
          }
          const completeResponse = await fetch(directUrl, {
            method: "POST",
            credentials: "same-origin",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ action: "complete", token: session.token }),
          });
          if (!completeResponse.ok) throw new Error("mail-send-service-unavailable");
          const result = await completeResponse.json() as { id?: number };
          if (!Number.isInteger(result.id) || Number(result.id) < 1) {
            throw new Error("mail-send-request-failed");
          }
          completed = true;
          largeAttachmentUploadIds.push(Number(result.id));
        } finally {
          if (!completed) {
            await fetch(directUrl, {
              method: "DELETE",
              credentials: "same-origin",
              headers: { "Content-Type": "application/json" },
              body: JSON.stringify({ token: session.token }),
            }).catch(() => undefined);
          }
        }
      }

      requestFormData.set("intent", intent);
      requestFormData.set("subject", effectiveSubject);
      requestFormData.set("firstAttachmentFilename", firstAttachmentFilename ?? "");
      requestFormData.set("largeAttachmentUploadIds", largeAttachmentUploadIds.join(","));
      requestFormData.set("attachmentModes", regularFiles.map((attachment) => attachment.mode).join(","));
      regularFiles.forEach((attachment) => {
        requestFormData.append("attachmentFiles", attachment.file, attachment.filename);
      });

      // Keep the same overlay active after uploading until the durable send
      // request is accepted. SMTP delivery continues in the background.
      if (intent === "send" && regularFiles.length === 0) {
        setSendModalState((current) => current ? { ...current, stage: "sending" } : current);
      }

      await new Promise<void>((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        let pendingChunk = "";
        let consumedResponseLength = 0;
        let receivedTerminalEvent = false;
        let responseParseFailed = false;

        const handleStreamLine = (line: string) => {
          const event = JSON.parse(line) as
            | {
                redirectTo: string;
                type: "done" | "error";
              }
            | {
                snapshot: ComposeSendProgressSnapshot;
                type: "upload-progress";
              }
            | {
                completedCount: number;
                totalCount: number;
                type: "individual-progress";
              }
            | {
                stage: "sending" | "uploading";
                type: "stage";
              }
            | {
                type: "heartbeat";
              };

          if (event.type === "heartbeat") return;

          if (event.type === "upload-progress") {
            setSendModalState((current) => ({
              individualProgress: current?.individualProgress ?? initialIndividualProgress,
              snapshot: event.snapshot,
              stage: "uploading",
            }));
            return;
          }

          if (event.type === "individual-progress") {
            setSendModalState((current) => ({
              individualProgress: {
                completedCount: event.completedCount,
                totalCount: event.totalCount,
              },
              snapshot: current?.snapshot ?? initialSendSnapshot,
              stage: "sending",
            }));
            return;
          }

          if (event.type === "stage") {
            setSendModalState((current) => ({
              individualProgress: current?.individualProgress ?? initialIndividualProgress,
              snapshot: current?.snapshot ?? createEmptyComposeSendProgressSnapshot(),
              stage: event.stage,
            }));
            return;
          }

          if (event.type === "done") {
            exitSave.dismiss();
            receivedTerminalEvent = true;
            setSendModalState((current) => ({
              individualProgress: current?.individualProgress
                ? {
                    completedCount: current.individualProgress.totalCount,
                    totalCount: current.individualProgress.totalCount,
                  }
                : initialIndividualProgress,
              snapshot: current?.snapshot ?? createEmptyComposeSendProgressSnapshot(),
              stage: "complete",
            }));
            if (intent === "send") {
              router.push(event.redirectTo);
            } else {
              sendRedirectTimerRef.current = window.setTimeout(() => {
                router.push(event.redirectTo);
              }, 320);
            }
            return;
          }

          if (event.type !== "error") return;
          receivedTerminalEvent = true;
          setSendModalState((current) => ({
            errorMessage: getComposeSendErrorMessage(event.redirectTo, t),
            individualProgress: current?.individualProgress ?? initialIndividualProgress,
            snapshot: markIncompleteComposeUploadsFailed(current?.snapshot ?? initialSendSnapshot),
            stage: "error",
          }));
        };

        const consumeResponseText = () => {
          const nextChunk = xhr.responseText.slice(consumedResponseLength);

          if (!nextChunk) {
            return;
          }

          consumedResponseLength = xhr.responseText.length;
          pendingChunk += nextChunk;
          const lines = pendingChunk.split("\n");
          pendingChunk = lines.pop() ?? "";

          for (const line of lines) {
            const normalizedLine = line.trim();

            if (!normalizedLine) {
              continue;
            }

            try {
              handleStreamLine(normalizedLine);
            } catch {
              responseParseFailed = true;
            }
          }
        };

        xhr.open("POST", "/api/mailbox/compose-send");

        xhr.upload.addEventListener("progress", (event) => {
          if (regularFiles.length === 0) {
            return;
          }

          const uploadRatio =
            event.lengthComputable && event.total > 0 ? event.loaded / event.total : 0;
          let remainingBytes = Math.round(
            regularFiles.reduce((sum, attachment) => sum + attachment.sizeBytes, 0) *
              clampProgressRatio(uploadRatio),
          );
          for (const attachment of regularFiles) {
            const uploadedBytes = Math.min(attachment.sizeBytes, remainingBytes);
            uploadedById.set(attachment.id, uploadedBytes);
            remainingBytes -= uploadedBytes;
          }
          setSendModalState((current) => ({
            individualProgress: current?.individualProgress ?? initialIndividualProgress,
            snapshot: applyFileUploadProgress(initialSendSnapshot, uploadedById),
            stage: "uploading",
          }));
        });

        xhr.upload.addEventListener("load", () => {
          if (regularFiles.length === 0) {
            return;
          }

          regularFiles.forEach((attachment) => uploadedById.set(attachment.id, attachment.sizeBytes));

          setSendModalState((current) => ({
            individualProgress: current?.individualProgress ?? initialIndividualProgress,
            snapshot: applyFileUploadProgress(initialSendSnapshot, uploadedById),
            stage: "sending",
          }));
        });

        xhr.addEventListener("progress", consumeResponseText);
        xhr.addEventListener("readystatechange", () => {
          if (xhr.readyState >= XMLHttpRequest.LOADING) {
            consumeResponseText();
          }
        });

        xhr.addEventListener("load", () => {
          consumeResponseText();

          const trailingLine = pendingChunk.trim();

          if (trailingLine) {
            try {
              handleStreamLine(trailingLine);
            } catch {
              responseParseFailed = true;
            }
            pendingChunk = "";
          }

          if (xhr.status >= 200 && xhr.status < 300) {
            if (receivedTerminalEvent && !responseParseFailed) {
              resolve();
            } else {
              reject(new Error("mail-send-response-invalid"));
            }
            return;
          }

          try {
            const payload = JSON.parse(xhr.responseText) as { redirectTo?: string } | null;

            if (payload?.redirectTo) {
              if (xhr.status === 401) {
                setSendModalState(null);
                void exitSave.saveForSessionExit().finally(() => router.push(payload.redirectTo as string));
              } else {
                setSendModalState((current) => ({
                  errorMessage: getComposeSendErrorMessage(payload.redirectTo as string, t),
                  individualProgress: current?.individualProgress ?? initialIndividualProgress,
                  snapshot: markIncompleteComposeUploadsFailed(current?.snapshot ?? initialSendSnapshot),
                  stage: "error",
                }));
              }
              resolve();
              return;
            }
          } catch {
            // ignore malformed error payloads
          }

          if (xhr.status === 413) {
            reject(new Error("mail-send-request-too-large"));
          } else if ([502, 503, 504].includes(xhr.status)) {
            reject(new Error("mail-send-service-unavailable"));
          } else {
            reject(new Error("mail-send-request-failed"));
          }
        });

        xhr.addEventListener("error", () => {
          reject(new Error("mail-send-network-failed"));
        });

        xhr.addEventListener("abort", () => {
          reject(new Error("mail-send-request-aborted"));
        });

        sendRequestStarted = true;
        xhr.send(requestFormData);
      });
    } catch (error) {
      if (!sendRequestStarted && largeAttachmentUploadIds.length > 0) {
        await fetch("/api/mailbox/uploads/large", {
          method: "DELETE",
          credentials: "same-origin",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ ids: largeAttachmentUploadIds }),
        }).catch(() => undefined);
      }
      setSendModalState((current) => ({
        errorMessage: getComposeRequestFailureMessage(error, t),
        individualProgress: current?.individualProgress ?? initialIndividualProgress,
        snapshot: markIncompleteComposeUploadsFailed(current?.snapshot ?? initialSendSnapshot),
        stage: "error",
      }));
    } finally {
      sendRequestLockedRef.current = false;
    }
  };

  const handleComposeSubmit = (event: FormEvent<HTMLFormElement>) => {
    if (submitLocked) {
      event.preventDefault();
      return;
    }

    const submitter = (event.nativeEvent as SubmitEvent).submitter as
      | HTMLButtonElement
      | HTMLInputElement
      | null;

    if (submitter?.getAttribute("name") === "intent" && submitter.getAttribute("value") === "draft") {
      event.preventDefault();
      void handleSend("draft");
      return;
    }

    event.preventDefault();
    void handleSend("send");
  };

  const sendSnapshot = sendModalState?.snapshot ?? createEmptyComposeSendProgressSnapshot();
  const individualSendProgress = sendModalState?.individualProgress;
  const hasIndividualSendProgress = Boolean(
    individualSendProgress && individualSendProgress.totalCount > 1,
  );
  const sendProgressPercent =
    sendModalState?.stage === "uploading" && sendSnapshot.totalBytes > 0
      ? Math.round((sendSnapshot.completedBytes / sendSnapshot.totalBytes) * 100)
      : hasIndividualSendProgress && individualSendProgress
        ? Math.round(
            (individualSendProgress.completedCount / individualSendProgress.totalCount) * 100,
          )
        : sendModalState?.stage === "sending" || sendModalState?.stage === "complete"
          ? 100
          : 0;
  const hasSendAttachments = sendSnapshot.items.length > 0;
  const sendModalTitle = submitIntent === "draft"
    ? sendModalState?.stage === "uploading"
      ? t("compose.send.upload")
      : sendModalState?.stage === "complete"
        ? t("compose.draft.complete")
        : t("compose.draft.progress")
    : sendModalState?.stage === "error"
      ? t("compose.error.title")
      : sendModalState?.stage === "uploading"
        ? t("compose.send.upload")
        : sendModalState?.stage === "complete"
          ? hasIndividualSendProgress
            ? t("compose.send.individualComplete")
            : t("compose.send.complete")
          : hasIndividualSendProgress
            ? t("compose.send.individualProgress")
            : t("compose.send.progress");
  const sendStageMessage = submitIntent === "draft"
    ? sendModalState?.stage === "uploading"
      ? t("compose.upload.description")
      : sendModalState?.stage === "complete"
        ? t("compose.draft.completeDescription")
        : sendModalState?.stage === "error"
          ? t("compose.draft.errorCheck")
          : t("compose.draft.progress")
    : sendModalState?.stage === "uploading"
      ? t("compose.upload.description")
      : sendModalState?.stage === "sending"
        ? hasIndividualSendProgress
          ? t("compose.send.individualProgress")
          : t("compose.sending")
        : sendModalState?.stage === "complete"
          ? hasIndividualSendProgress
            ? t("compose.send.individualCompleteDescription")
            : t("compose.send.completeDescription")
          : sendModalState?.stage === "error"
            ? t("compose.send.errorCheck")
            : "";

  return (
    <form
      ref={formRef}
      data-mail-compose="true"
      inert={exitSave.saving}
      action={submitMailboxMessageAction}
      onSubmit={handleComposeSubmit}
      aria-busy={isSendProcessing || exitSave.saving}
      className="flex h-full min-h-0 flex-col overflow-y-auto overscroll-contain"
    >
      {exitSave.saving ? <MailComposeSendingOverlay complete={false} label={t("compose.draft.progress")} /> : null}
      <input type="hidden" name="folder" value={currentFolder} />
      <input
        type="hidden"
        name="sourceDraftMessageId"
        value={sourceDraftMessageId ? String(sourceDraftMessageId) : ""}
      />
      <input type="hidden" name="body" value={bodyText} />
      <input type="hidden" name="bodyHtml" value={effectiveBodyHtml} />
      <input
        type="hidden"
        name="inlineImageUploadIds"
        value={inlineImageUploads.map((upload) => String(upload.id)).join(",")}
      />

      <input
        ref={attachmentInputRef}
        type="file"
        multiple
        className="hidden"
        onChange={(event) => {
          const files = Array.from(event.currentTarget.files ?? []);
          event.currentTarget.value = "";
          void handleAttachmentFilesRef.current(files);
        }}
      />
      <input
        ref={imageInputRef}
        type="file"
        accept="image/*"
        multiple
        className="hidden"
        onChange={(event) => {
          const files = Array.from(event.currentTarget.files ?? []);
          event.currentTarget.value = "";
          void handleInlineImageFilesRef.current(files);
        }}
      />

      <div className="relative border-b border-[#edf1f6] px-3 pb-3 pt-4 sm:px-4 lg:px-8 lg:pb-3 lg:pt-6">
        <div className="flex items-center pr-[148px] text-[15px] leading-none sm:pr-[180px] sm:text-[16px]">
          <h2 className="truncate text-[16px] font-semibold tracking-[-0.03em] text-[#111a33] sm:text-[17px]">
            {composeTitle}
          </h2>
          <div className={composeActionGroupClassName}>
            <button
              type="submit"
              disabled={submitLocked}
              className={`cursor-pointer disabled:cursor-not-allowed ${composePrimaryActionButtonClassName}`}
            >
              {t("compose.send")}
            </button>
            <button
              type="submit"
              name="intent"
              value="draft"
              disabled={submitLocked}
              className={composeSecondaryActionButtonClassName}
            >
              {composeMode === "draft" ? t("compose.draft.update") : t("compose.draft.save")}
            </button>
          </div>
        </div>
      </div>

      <div className="grid shrink-0 gap-3.5 border-b border-[#edf2fb] px-4 py-5 sm:px-8 sm:py-6">
        <RecipientAutocompleteField
          ref={toRecipientFieldRef}
          autoFocus={!shouldFocusEditor}
          defaultValue={defaultTo}
          endAction={(
            <button
              type="button"
              onClick={openAddressBook}
              className="inline-flex h-10 cursor-pointer items-center gap-1.5 rounded-[10px] border border-[#d8e3f4] bg-white px-3 text-[12px] font-semibold leading-none text-[#506582] transition-colors hover:border-[#bad0fb] hover:bg-[#f6f9ff] hover:text-[#2f6df6]"
            >
              <svg viewBox="0 0 24 24" fill="none" className="h-4 w-4" aria-hidden="true">
                <path d="M7 5.5h10A1.5 1.5 0 0 1 18.5 7v12H7A1.5 1.5 0 0 1 5.5 17.5V7A1.5 1.5 0 0 1 7 5.5Z" stroke="currentColor" strokeWidth="1.6" />
                <path d="M8.5 5.5V3.8M15.5 5.5V3.8M8.7 10h6.6M8.7 13.5h4.4" stroke="currentColor" strokeLinecap="round" strokeWidth="1.6" />
              </svg>
              <span>주소록</span>
            </button>
          )}
          errorMessage={validationErrors.recipient}
          focusSignal={recipientFocusSignal}
          inputId={`${toolbarId}-to`}
          label={t("compose.to")}
          name="to"
          onInteract={() => clearValidationError("recipient")}
          onSendIndividuallyChange={setSendIndividually}
          placeholder={t("compose.to.placeholder")}
          sendIndividually={sendIndividually}
        />

        <RecipientAutocompleteField
          ref={ccRecipientFieldRef}
          defaultValue={defaultCc}
          inputId={`${toolbarId}-cc`}
          label={t("compose.cc")}
          name="cc"
          onInteract={() => clearValidationError("recipient")}
          placeholder={t("compose.cc.placeholder")}
        />

        <RecipientAutocompleteField
          ref={bccRecipientFieldRef}
          defaultValue={defaultBcc}
          inputId={`${toolbarId}-bcc`}
          label={t("compose.bcc")}
          name="bcc"
          onInteract={() => clearValidationError("recipient")}
          placeholder={t("compose.bcc.placeholder")}
        />

        <div className="grid grid-cols-1 items-start gap-1.5 sm:grid-cols-[88px_minmax(0,1fr)] sm:gap-3">
          <label
            htmlFor={`${toolbarId}-subject`}
            className="text-[13px] font-medium text-[#6f809d] sm:pt-2.5"
          >
            {t("compose.subject")}
          </label>
          <div>
            <input
              id={`${toolbarId}-subject`}
              ref={subjectInputRef}
              name="subject"
              type="text"
              maxLength={255}
              value={subject}
              aria-describedby={validationErrors.subject ? `${toolbarId}-subject-error` : undefined}
              aria-invalid={Boolean(validationErrors.subject)}
              onChange={(event) => {
                setSubject(event.target.value);
                clearValidationError("subject");
              }}
              placeholder={retainedDraftAttachments[0]?.filename ?? attachmentFiles[0]?.filename ?? t("compose.subject.placeholder")}
              className={`h-10 w-full rounded-[10px] border px-3.5 text-[14px] text-[#1e2a43] outline-none placeholder:text-[#a0adc2] transition-colors ${
                validationErrors.subject
                  ? "border-[#e45b5b] bg-[#fffafa] shadow-[0_0_0_3px_rgba(228,91,91,0.08)] focus:border-[#d94848] focus:bg-white"
                  : "border-[#e2eaf8] bg-[#fbfcff] focus:border-[#cbdcff] focus:bg-white"
              }`}
            />
            {validationErrors.subject ? (
              <p
                id={`${toolbarId}-subject-error`}
                role="alert"
                className="mt-1.5 text-[12px] font-medium text-[#d24747]"
              >
                {validationErrors.subject}
              </p>
            ) : null}
          </div>
        </div>

        <div className="grid grid-cols-1 items-start gap-1.5 border-t border-[#edf2fb] pt-5 sm:grid-cols-[88px_minmax(0,1fr)] sm:gap-3">
          <div className="text-[13px] font-medium text-[#6f809d] sm:pt-3">{t("compose.attachment")}</div>
          <div className="space-y-3">
            <div
              role="button"
              tabIndex={0}
              onClick={() => attachmentInputRef.current?.click()}
              onKeyDown={(event) => {
                if (event.key === "Enter" || event.key === " ") {
                  event.preventDefault();
                  attachmentInputRef.current?.click();
                }
              }}
              onDragEnter={(event) => {
                event.preventDefault();
                setAttachmentDragActive(true);
              }}
              onDragOver={(event) => {
                event.preventDefault();
                setAttachmentDragActive(true);
              }}
              onDragLeave={(event) => {
                event.preventDefault();
                if (event.currentTarget.contains(event.relatedTarget as Node | null)) {
                  return;
                }
                setAttachmentDragActive(false);
              }}
              onDrop={(event) => {
                event.preventDefault();
                setAttachmentDragActive(false);
                void handleAttachmentFilesRef.current(Array.from(event.dataTransfer.files ?? []));
              }}
              className={`flex h-14 w-full cursor-pointer items-center justify-center rounded-[8px] border border-dashed px-5 text-center transition-all duration-200 ${
                attachmentDragActive
                  ? "border-[#7ea5ff] bg-[#f4f8ff] text-[#2f6df6] shadow-[0_16px_32px_rgba(47,109,246,0.08)]"
                  : "border-[#d9e1ee] bg-white text-[#7b879a] hover:border-[#c7d4ea] hover:bg-[#fbfcfe]"
              }`}
            >
              <div className="flex min-w-0 items-center justify-center gap-2 text-[14px]">
                <svg
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="1.7"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  className="h-[16px] w-[16px] shrink-0"
                  aria-hidden="true"
                >
                  <path d="M12 7v10" />
                  <path d="M7 12h10" />
                  <path d="M6.8 4.8h7l3 3V19a1.8 1.8 0 0 1-1.8 1.8H6.8A1.8 1.8 0 0 1 5 19V6.6a1.8 1.8 0 0 1 1.8-1.8Z" />
                  <path d="M13.8 4.8v3.4h3.4" />
                </svg>
                <span className="truncate">{t("compose.attachment.drag")}</span>
              </div>
            </div>

            {attachmentFiles.length > 0 || retainedDraftAttachments.length > 0 ? (
              <div className="overflow-hidden rounded-[10px] border border-[#e2e9f5] bg-white">
                <div className="flex flex-wrap items-center justify-between gap-2 border-b border-[#eef2f8] px-4 py-2 text-[12px] text-[#71819a]">
                  <span>일반 {formatUploadSize(regularAttachmentBytes)}/{formatUploadSize(growth ? GROWTH_REGULAR_ATTACHMENT_LIMIT_BYTES : FREE_REGULAR_ATTACHMENT_LIMIT_BYTES)}</span>
                  {growth ? <span>대용량 {formatUploadSize(largeAttachmentBytes)} · 파일당 250MB</span> : null}
                </div>
                <div className="hidden grid-cols-[minmax(0,1fr)_100px_90px_160px] gap-3 border-b border-[#eef2f8] bg-[#fafcff] px-4 py-2 text-[11px] text-[#8190a8] lg:grid">
                  <span>파일명</span><span>업로드 상태</span><span>용량</span><span>첨부 방식</span>
                </div>
                {retainedDraftAttachments.map((attachment, index) => (
                  <div
                    key={`draft:${attachment.index}:${attachment.filename}`}
                    className={`flex flex-wrap items-center gap-3 px-4 py-2.5 lg:flex-nowrap ${index === 0 ? "" : "border-t border-[#eef2f8]"}`}
                  >
                    <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-[7px] bg-[#f4f8ff] text-[#5f7fc6]">
                      <AttachmentIcon isImage={attachment.contentType.startsWith("image/")} />
                    </div>
                    <div className="min-w-0 flex-1">
                      <p className="truncate text-[13px] font-medium text-[#1f2d46]">{attachment.filename}</p>
                      <p className="text-[11px] text-[#7a8ba7] lg:hidden">
                        {formatUploadSize(attachment.sizeBytes)} · {attachment.isLargeAttachment ? "대용량 첨부" : "일반 첨부"}
                      </p>
                    </div>
                    <span className="hidden w-[100px] shrink-0 text-[11px] text-[#7a8ba7] lg:block">초안에 저장됨</span>
                    <span className="hidden w-[90px] shrink-0 text-[12px] text-[#425575] lg:block">{formatUploadSize(attachment.sizeBytes)}</span>
                    <span className="min-w-[160px] text-[11px] text-[#70819b]">{attachment.isLargeAttachment ? "대용량 첨부" : "일반 첨부"}</span>
                  </div>
                ))}
                {attachmentFiles.map((upload, index) => (
                  <div
                    key={upload.id}
                    className={`flex flex-wrap items-center gap-3 px-4 py-2.5 lg:flex-nowrap ${
                      index === 0 && retainedDraftAttachments.length === 0 ? "" : "border-t border-[#eef2f8]"
                    }`}
                  >
                    <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-[7px] bg-[#f4f8ff] text-[#5f7fc6]">
                      <AttachmentIcon isImage={upload.isImage} />
                    </div>

                    <div className="min-w-0 flex-1">
                      <p className="truncate text-[13px] font-medium text-[#1f2d46]">
                        {upload.filename}
                      </p>
                      <p className="text-[11px] text-[#7a8ba7] lg:hidden">
                        {formatUploadSize(upload.sizeBytes)} · {upload.mode === "regular" ? "일반 첨부" : "대용량 첨부"}
                      </p>
                    </div>
                    <span className="hidden w-[100px] shrink-0 text-[11px] text-[#7a8ba7] lg:block">발송 대기</span>
                    <span className="hidden w-[90px] shrink-0 text-[12px] text-[#425575] lg:block">{formatUploadSize(upload.sizeBytes)}</span>
                    <div className="flex shrink-0 items-center gap-1">
                      {growth ? (
                        <button
                          type="button"
                          disabled={upload.mode === "auto_large" ||
                            (upload.mode === "user_selected_large" &&
                              !canRestoreRegularAttachment({
                                mode: upload.mode,
                                fileSizeBytes: upload.sizeBytes,
                                regularTotalBytes: regularAttachmentBytes,
                                finalized: false,
                              }))}
                          title={upload.mode === "auto_large"
                            ? "용량 초과로 자동 전환된 대용량 첨부는 일반 첨부로 변경할 수 없습니다."
                            : upload.mode === "user_selected_large" &&
                              regularAttachmentBytes + upload.sizeBytes > GROWTH_REGULAR_ATTACHMENT_LIMIT_BYTES
                              ? "현재 메일의 일반 첨부 용량을 초과하여 전환할 수 없습니다."
                              : undefined}
                          onClick={() => toggleAttachmentMode(upload)}
                          className="rounded-[7px] border border-[#dce5f4] px-2 py-1 text-[11px] text-[#526784] hover:bg-[#f7faff] disabled:cursor-not-allowed disabled:opacity-45"
                        >
                          {upload.mode === "regular" ? "대용량 전환" : "일반 전환"}
                        </button>
                      ) : null}
                      {upload.isImage ? (
                        <button
                          type="button"
                          onClick={() => openAttachmentPreview(upload)}
                          className="inline-flex h-9 items-center justify-center rounded-[12px] px-3 text-[12px] font-medium text-[#516684] transition-colors hover:bg-[#f4f7fc] hover:text-[#2f6df6]"
                        >
                          {t("compose.file.view")}
                        </button>
                      ) : null}
                      <button
                        type="button"
                        onClick={() => void removeAttachmentUpload(upload)}
                        className="inline-flex h-9 items-center justify-center rounded-[12px] px-3 text-[12px] font-medium text-[#8b97aa] transition-colors hover:bg-[#fff4f4] hover:text-[#e25656]"
                      >
                        {t("compose.file.remove")}
                      </button>
                    </div>
                  </div>
                ))}
                {growth && largeAttachmentBytes > 0 ? (
                  <p className="border-t border-[#eef2f8] px-4 py-2 text-right text-[11px] text-[#7a8ba7]">
                    대용량 첨부는 30일 보관, 최대 100회 다운로드할 수 있습니다.
                  </p>
                ) : null}
              </div>
            ) : null}

            {uploadError ? <p className="text-[13px] text-[#d24747]">{uploadError}</p> : null}
          </div>
        </div>
      </div>

      <div className="min-h-0 flex-1 px-4 py-5 sm:px-8 sm:py-7">
        <div className="flex h-full min-h-[560px] flex-col sm:min-h-[640px]">
          <div
            ref={editorShellRef}
            tabIndex={-1}
            aria-describedby={validationErrors.body ? `${toolbarId}-body-error` : undefined}
            aria-invalid={Boolean(validationErrors.body)}
            className={`official-compose-editor relative flex min-h-[530px] flex-1 flex-col overflow-visible rounded-[10px] border bg-[#fcfdff] transition-colors sm:min-h-[610px] ${
              validationErrors.body
                ? "border-[#e45b5b] shadow-[0_0_0_3px_rgba(228,91,91,0.08)]"
                : "border-[#e5ecf9]"
            }`}
          >
            <>
              <div className="flex min-h-14 flex-wrap items-center gap-3 rounded-t-[10px] bg-white px-3 py-2.5 sm:px-4">
                {htmlEditorViewMode === "visual" ? (
                  <div className="flex min-w-0 flex-1 flex-wrap items-center gap-1.5">
                    <select
                      value={fontFamily}
                      aria-label={t("compose.editor.font")}
                      title={t("compose.editor.font")}
                      onMouseDown={capturePreservedHtmlSelection}
                      onChange={(event) => {
                        const nextValue = event.currentTarget.value;
                        setFontFamily(nextValue);
                        applyPreservedHtmlCommand("fontName", nextValue);
                      }}
                      className="h-8 max-w-[132px] cursor-pointer rounded-[8px] border border-[#e2eaf8] bg-white px-2 text-[12px] font-medium text-[#425575] outline-none transition-colors focus:border-[#bfd2f6]"
                    >
                      <option value="Pretendard">프리텐다드</option>
                      <option value="Arial">Arial</option>
                      <option value="Malgun Gothic">맑은 고딕</option>
                      <option value="Nanum Gothic">나눔고딕</option>
                      <option value="Georgia">Georgia</option>
                      <option value="monospace">Monospace</option>
                    </select>
                    <select
                      value={fontSize}
                      aria-label={t("compose.editor.fontSize")}
                      title={t("compose.editor.fontSize")}
                      onMouseDown={capturePreservedHtmlSelection}
                      onChange={(event) => {
                        const nextValue = event.currentTarget.value;
                        setFontSize(nextValue);
                        applyPreservedHtmlFontSize(nextValue);
                      }}
                      className="h-8 w-[72px] cursor-pointer rounded-[8px] border border-[#e2eaf8] bg-white px-2 text-[12px] font-medium text-[#425575] outline-none transition-colors focus:border-[#bfd2f6]"
                    >
                      <option value="13px">13px</option>
                      <option value="15px">15px</option>
                      <option value="17px">17px</option>
                      <option value="19px">19px</option>
                      <option value="24px">24px</option>
                      <option value="32px">32px</option>
                    </select>
                    <span className="mx-0.5 h-5 w-px bg-[#e4eaf3]" />
                    {([
                      ["bold", "B", t("compose.editor.bold"), "font-bold"],
                      ["italic", "I", t("compose.editor.italic"), "italic"],
                      ["underline", "U", t("compose.editor.underline"), "underline"],
                      ["strikeThrough", "S", t("compose.editor.strike"), "line-through"],
                    ] as const).map(([command, label, title, className]) => (
                      <button
                        key={command}
                        type="button"
                        aria-label={title}
                        title={title}
                        onMouseDown={(event) => event.preventDefault()}
                        onClick={() => applyPreservedHtmlCommand(command)}
                        className={`inline-flex h-8 w-8 cursor-pointer items-center justify-center text-[13px] text-[#536782] transition-colors hover:text-[#2f6df6] ${className}`}
                      >
                        {label}
                      </button>
                    ))}
                    <label
                      className="relative inline-flex h-8 w-9 cursor-pointer items-center justify-center"
                      title={t("compose.editor.textColor")}
                      onMouseDown={capturePreservedHtmlSelection}
                    >
                      <input
                        type="color"
                        value={textColor}
                        aria-label={t("compose.editor.textColor")}
                        className="absolute inset-0 cursor-pointer opacity-0"
                        onChange={(event) => {
                          const nextValue = event.currentTarget.value;
                          setTextColor(nextValue);
                          applyPreservedHtmlCommand("foreColor", nextValue);
                        }}
                      />
                      <span className="text-[13px] font-bold text-[#536782]">A</span>
                      <span
                        className="absolute bottom-1 h-0.5 w-4 rounded-full"
                        style={{ backgroundColor: textColor }}
                      />
                    </label>
                    <label
                      className="relative inline-flex h-8 w-9 cursor-pointer items-center justify-center"
                      title={t("compose.editor.highlightColor")}
                      onMouseDown={capturePreservedHtmlSelection}
                    >
                      <input
                        type="color"
                        value={backgroundColor}
                        aria-label={t("compose.editor.highlightColor")}
                        className="absolute inset-0 cursor-pointer opacity-0"
                        onChange={(event) => {
                          const nextValue = event.currentTarget.value;
                          setBackgroundColor(nextValue);
                          applyPreservedHtmlCommand("hiliteColor", nextValue);
                        }}
                      />
                      <span
                        className="inline-flex h-5 min-w-6 items-center justify-center rounded-[4px] px-1 text-[10px] font-bold text-[#536782]"
                        style={{ backgroundColor }}
                      >
                        Bg
                      </span>
                    </label>
                    <span className="mx-0.5 h-5 w-px bg-[#e4eaf3]" />
                    <button
                      type="button"
                      aria-label={t("compose.editor.list.ordered")}
                      title={t("compose.editor.list.ordered")}
                      onMouseDown={(event) => event.preventDefault()}
                      onClick={() => applyPreservedHtmlCommand("insertOrderedList")}
                      className="inline-flex h-8 min-w-8 cursor-pointer items-center justify-center px-1 text-[12px] font-semibold text-[#536782] transition-colors hover:text-[#2f6df6]"
                    >
                      1.
                    </button>
                    <button
                      type="button"
                      aria-label={t("compose.editor.list.bullet")}
                      title={t("compose.editor.list.bullet")}
                      onMouseDown={(event) => event.preventDefault()}
                      onClick={() => applyPreservedHtmlCommand("insertUnorderedList")}
                      className="inline-flex h-8 min-w-8 cursor-pointer items-center justify-center px-1 text-[16px] font-semibold text-[#536782] transition-colors hover:text-[#2f6df6]"
                    >
                      •
                    </button>
                    <button
                      type="button"
                      aria-label={t("compose.editor.clear")}
                      title={t("compose.editor.clear")}
                      onMouseDown={(event) => event.preventDefault()}
                      onClick={() => applyPreservedHtmlCommand("removeFormat")}
                      className="inline-flex h-8 cursor-pointer items-center justify-center px-1.5 text-[11px] font-semibold text-[#71819a] transition-colors hover:text-[#2f6df6]"
                    >
                      {t("compose.editor.clear")}
                    </button>
                    <span className="mx-0.5 h-5 w-px bg-[#e4eaf3]" />
                    <button
                      type="button"
                      onMouseDown={(event) => {
                        event.preventDefault();
                        capturePreservedHtmlSelection();
                      }}
                      onClick={() => openHtmlLinkDialog()}
                      className="inline-flex h-8 cursor-pointer items-center justify-center px-1.5 text-[11px] font-semibold text-[#526784] transition-colors hover:text-[#2f6df6]"
                    >
                      {t("compose.editor.link.add")}
                    </button>
                    <button
                      type="button"
                      onMouseDown={(event) => {
                        event.preventDefault();
                        capturePreservedHtmlSelection();
                      }}
                      onClick={() => imageInputRef.current?.click()}
                      className="inline-flex h-8 cursor-pointer items-center justify-center px-1.5 text-[11px] font-semibold text-[#526784] transition-colors hover:text-[#2f6df6]"
                    >
                      {t("compose.editor.image.attach")}
                    </button>
                  </div>
                ) : (
                  <div className="min-w-0 flex-1">
                    <p className="text-[12px] font-semibold text-[#526784]">
                      {t("compose.editor.html.code")}
                    </p>
                  </div>
                )}
                <div className="ml-auto flex shrink-0 items-center gap-2">
                  {htmlEditorViewMode === "visual" ? (
                    <div
                      className="inline-flex h-9 items-center p-1"
                      aria-label={t("compose.editor.html.viewport")}
                    >
                      {([
                        ["small", t("compose.editor.viewport.small")],
                        ["medium", t("compose.editor.viewport.medium")],
                        ["large", t("compose.editor.viewport.large")],
                      ] as const).map(([value, label]) => {
                        const active = htmlEditorViewportSize === value;
                        return (
                          <button
                            key={value}
                            type="button"
                            aria-label={label}
                            aria-pressed={active}
                            title={label}
                            onClick={() => setHtmlEditorViewportSize(value)}
                            className={`inline-flex h-7 w-8 cursor-pointer items-center justify-center transition-colors ${
                              active
                                ? "text-[#2f6df6]"
                                : "text-[#7a8ba4] hover:text-[#526784]"
                            }`}
                          >
                            <HtmlViewportIcon size={value} />
                          </button>
                        );
                      })}
                    </div>
                  ) : null}
                  <button
                    type="button"
                    aria-label={htmlEditorViewMode === "source" ? t("compose.editor.html.preview") : t("compose.editor.html.source")}
                    aria-pressed={htmlEditorViewMode === "source"}
                    title={htmlEditorViewMode === "source" ? t("compose.editor.html.preview") : t("compose.editor.html.source")}
                    onClick={() => {
                      hideHtmlLinkPopover();

                      if (htmlEditorViewMode === "source") {
                        const preparedHtml = prepareEditableEmailHtml(htmlSource);

                        if (preparedHtml) {
                          setBodyHtml(preparedHtml.html);
                          setBodyText(preparedHtml.text);
                          setHtmlSource(getPreservedHtmlEditorSource(preparedHtml.html));
                        }

                        setHtmlEditorViewMode("visual");
                        return;
                      }

                      syncPreservedHtmlEditor();
                      setHtmlEditorViewMode("source");
                    }}
                    className={`inline-flex h-9 w-10 shrink-0 cursor-pointer items-center justify-center font-mono text-[12px] font-bold tracking-[-0.08em] transition-colors ${
                      htmlEditorViewMode === "source"
                        ? "text-[#2f6df6]"
                        : "text-[#667792] hover:text-[#2f6df6]"
                    }`}
                  >
                    {"</>"}
                  </button>
                </div>
              </div>
              <div className="relative min-h-0 flex-1 overflow-auto rounded-b-[10px] bg-[#fcfdff]">
                {htmlEditorViewMode === "source" ? (
                  <textarea
                    ref={htmlSourceEditorRef}
                    value={htmlSource}
                    aria-label={t("compose.editor.html.code")}
                    spellCheck={false}
                    onChange={(event) => {
                      const nextHtml = event.currentTarget.value;
                      setHtmlSource(nextHtml);
                      setBodyHtml(wrapPreservedHtmlEditorSource(nextHtml));
                      setBodyText(htmlToEditorText(nextHtml));
                      clearValidationError("body");
                    }}
                    className="min-h-[480px] w-full resize-none rounded-b-[10px] border-0 bg-[#101827] px-5 py-5 font-mono text-[13px] leading-6 text-[#dbe8ff] outline-none transition-shadow focus:shadow-[inset_0_0_0_2px_rgba(47,109,246,0.2)] sm:min-h-[550px]"
                  />
                ) : (
                  <div
                    key="preserved-html-surface"
                    className="mx-auto min-h-full bg-white transition-[width] duration-200 ease-out"
                    style={{
                      width:
                        htmlEditorViewportSize === "large"
                          ? "100%"
                          : htmlEditorViewportSize === "medium"
                            ? "640px"
                            : "360px",
                    }}
                  >
                    <iframe
                      ref={preservedHtmlEditorRef}
                      title={t("compose.editor.html.body")}
                      sandbox="allow-same-origin"
                      srcDoc={PRESERVED_HTML_EDITOR_DOCUMENT}
                      className="official-compose-html-editor block h-[520px] min-h-[480px] w-full rounded-b-[10px] border-0 bg-white text-left outline-none sm:h-[590px] sm:min-h-[550px]"
                    />
                  </div>
                )}
              </div>
              {htmlEditorViewMode === "visual" && htmlLinkPopover ? (
                <div
                  role="toolbar"
                  aria-label={t("compose.editor.link.tools")}
                  className="absolute z-40 flex max-w-[calc(100%_-_24px)] -translate-x-1/2 items-center gap-2 rounded-[8px] border border-[#cdd9eb] bg-white px-2.5 py-1.5 text-[11px] text-[#425575] shadow-[0_12px_30px_rgba(26,43,74,0.16)]"
                  style={{ left: htmlLinkPopover.left, top: htmlLinkPopover.top }}
                >
                  <span className="shrink-0 font-medium">{t("compose.editor.link.destination")}</span>
                  <a
                    href={htmlLinkPopover.href}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="max-w-[210px] truncate text-[#2f6df6] underline"
                  >
                    {htmlLinkPopover.href}
                  </a>
                  <span className="h-3 w-px bg-[#d9e2ef]" />
                  <button
                    type="button"
                    onClick={() => openHtmlLinkDialog(activeHtmlLinkRef.current)}
                    className="shrink-0 cursor-pointer font-semibold text-[#2f6df6]"
                  >
                    {t("compose.editor.link.change")}
                  </button>
                  <button
                    type="button"
                    onClick={removeActiveHtmlLink}
                    className="shrink-0 cursor-pointer font-semibold text-[#d24747]"
                  >
                    {t("compose.editor.link.remove")}
                  </button>
                </div>
              ) : null}
            </>
          </div>
          {validationErrors.body ? (
            <p
              id={`${toolbarId}-body-error`}
              role="alert"
              className="mt-2 text-[12px] font-medium text-[#d24747]"
            >
              {validationErrors.body}
            </p>
          ) : null}
        </div>
      </div>

      {htmlLinkDialog ? (
        <div
          className="fixed inset-0 z-[230] flex items-center justify-center bg-[rgba(15,23,42,0.18)] px-4 py-6 backdrop-blur-[2px]"
          onMouseDown={(event) => {
            if (event.target === event.currentTarget) {
              setHtmlLinkDialog(null);
              setHtmlLinkError(null);
            }
          }}
        >
          <div
            role="dialog"
            aria-modal="true"
            aria-labelledby={`${toolbarId}-link-dialog-title`}
            className="w-full max-w-[440px] rounded-[18px] border border-[#dce6f5] bg-white p-5 shadow-[0_26px_70px_rgba(15,23,42,0.2)]"
            onKeyDown={(event) => {
              if (event.key === "Escape") {
                setHtmlLinkDialog(null);
                setHtmlLinkError(null);
              } else if (event.key === "Enter") {
                event.preventDefault();
                saveHtmlLink();
              }
            }}
          >
            <div className="mb-5 flex items-start justify-between gap-4">
              <div>
                <h3
                  id={`${toolbarId}-link-dialog-title`}
                  className="text-[16px] font-semibold tracking-[-0.02em] text-[#17233d]"
                >
                  {htmlLinkDialog.mode === "edit"
                    ? t("compose.editor.link.editTitle")
                    : t("compose.editor.link.addTitle")}
                </h3>
                <p className="mt-1 text-[12px] text-[#7b8ba4]">
                  {t("compose.editor.link.description")}
                </p>
              </div>
              <button
                type="button"
                aria-label={t("common.close")}
                onClick={() => {
                  setHtmlLinkDialog(null);
                  setHtmlLinkError(null);
                }}
                className="inline-flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center text-[20px] font-light text-[#8795aa] transition-colors hover:text-[#31435f]"
              >
                ×
              </button>
            </div>
            <div className="space-y-4">
              <label className="block">
                <span className="mb-1.5 block text-[12px] font-semibold text-[#425575]">
                  {t("compose.editor.link.text")}
                </span>
                <input
                  autoFocus
                  type="text"
                  value={htmlLinkDialog.text}
                  placeholder={t("compose.editor.link.textPlaceholder")}
                  onChange={(event) => {
                    const nextValue = event.currentTarget.value;
                    setHtmlLinkDialog((current) =>
                      current ? { ...current, text: nextValue } : current,
                    );
                    setHtmlLinkError(null);
                  }}
                  className="h-11 w-full rounded-[11px] border border-[#dce6f5] bg-white px-3.5 text-[14px] text-[#1f2c45] outline-none transition-colors placeholder:text-[#a3afc1] focus:border-[#8fb2fb]"
                />
              </label>
              <label className="block">
                <span className="mb-1.5 block text-[12px] font-semibold text-[#425575]">
                  {t("compose.editor.link.url")}
                </span>
                <input
                  type="text"
                  inputMode="url"
                  value={htmlLinkDialog.href}
                  placeholder={t("compose.editor.link.urlPlaceholder")}
                  onChange={(event) => {
                    const nextValue = event.currentTarget.value;
                    setHtmlLinkDialog((current) =>
                      current ? { ...current, href: nextValue } : current,
                    );
                    setHtmlLinkError(null);
                  }}
                  className="h-11 w-full rounded-[11px] border border-[#dce6f5] bg-white px-3.5 text-[14px] text-[#1f2c45] outline-none transition-colors placeholder:text-[#a3afc1] focus:border-[#8fb2fb]"
                />
              </label>
            </div>
            {htmlLinkError ? (
              <p role="alert" className="mt-3 text-[12px] font-medium text-[#d24747]">
                {htmlLinkError}
              </p>
            ) : null}
            <div className="mt-5 flex justify-end gap-2">
              <button
                type="button"
                onClick={() => {
                  setHtmlLinkDialog(null);
                  setHtmlLinkError(null);
                }}
                className="inline-flex h-10 cursor-pointer items-center justify-center rounded-[10px] px-4 text-[13px] font-semibold text-[#65758f] transition-colors hover:bg-[#f3f6fb]"
              >
                {t("common.cancel")}
              </button>
              <button
                type="button"
                onClick={saveHtmlLink}
                className="inline-flex h-10 cursor-pointer items-center justify-center rounded-[10px] bg-[#2f6df6] px-5 text-[13px] font-semibold text-white transition-colors hover:bg-[#245de3]"
              >
                {htmlLinkDialog.mode === "edit"
                  ? t("compose.editor.link.update")
                  : t("compose.editor.link.insert")}
              </button>
            </div>
          </div>
        </div>
      ) : null}

      {addressBookOpen ? (
        <MailComposeAddressBookDialog
          initialSelection={addressBookInitialSelection}
          onClose={() => setAddressBookOpen(false)}
          onConfirm={applyAddressBookSelection}
        />
      ) : null}

      {submitIntent === "send" && (sendModalState?.stage === "sending" || sendModalState?.stage === "complete") ? (
        <MailComposeSendingOverlay complete={sendModalState.stage === "complete"} />
      ) : null}

      {sendModalState &&
      (submitIntent === "draft" ||
        sendModalState.stage === "uploading" ||
        sendModalState.stage === "error") ? createPortal(
        <div className="fixed inset-0 z-[12200] flex items-center justify-center bg-[rgba(15,23,42,0.18)] px-4 py-6 backdrop-blur-[2px] sm:px-6">
          <div
            className={`w-full overflow-hidden rounded-[18px] border border-[#e1e7f0] bg-white shadow-[0_24px_64px_rgba(15,23,42,0.18)] ${
              hasSendAttachments ? "max-w-[760px]" : "max-w-[420px]"
            }`}
          >
            <div className="px-5 py-4 sm:px-6 sm:py-5">
              <div className="flex items-start justify-between gap-4">
                <div className="min-w-0">
                  <p className="text-[18px] font-semibold tracking-[-0.035em] text-[#18243a]">
                    {sendModalTitle}
                  </p>
                  <p className="mt-1 text-[13px] leading-5 text-[#71819a]">
                    {sendStageMessage}
                  </p>
                </div>
                <div className="flex shrink-0 items-center gap-2">
                  <span
                    className={`text-[12px] font-semibold ${
                      sendModalState.stage === "complete"
                        ? "text-[#2f8a55]"
                        : sendModalState.stage === "error"
                          ? "text-[#c54848]"
                          : "text-[#2f6df6]"
                    }`}
                  >
                    {sendModalState.stage === "uploading"
                      ? t("compose.uploading")
                      : sendModalState.stage === "sending"
                        ? t("compose.processing")
                        : sendModalState.stage === "complete"
                          ? t("compose.complete")
                          : t("compose.error.short")}
                  </span>
                  {sendModalState.stage === "error" ? (
                    <CloseIconButton
                      ariaLabel={`${t("compose.error.title")} ${t("common.close")}`}
                      onClick={() => setSendModalState(null)}
                    />
                  ) : null}
                </div>
              </div>

              <div className="mt-4 flex items-end justify-between gap-4">
                <p className="min-w-0 text-[13px] font-semibold text-[#24324a]">
                  {sendModalState.stage === "uploading" && hasSendAttachments
                    ? `${sendSnapshot.uploadedCount} / ${sendSnapshot.totalCount} · ${formatUploadSize(sendSnapshot.completedBytes)} / ${formatUploadSize(sendSnapshot.totalBytes)}`
                    : hasIndividualSendProgress && individualSendProgress
                      ? t("compose.send.requestCount", {
                          current: individualSendProgress.completedCount,
                          total: individualSendProgress.totalCount,
                        })
                      : sendModalState.stage === "sending"
                        ? submitIntent === "draft" ? t("compose.draft.progress") : t("compose.send.serverWaiting")
                        : sendModalState.stage === "error"
                          ? submitIntent === "draft" ? t("compose.error.short") : t("compose.failed")
                          : submitIntent === "draft" ? t("compose.draft.complete") : t("compose.sent")}
                </p>
                <p className="shrink-0 text-[13px] font-semibold tabular-nums text-[#526784]">
                  {sendModalState.stage === "uploading" || hasIndividualSendProgress
                    ? `${sendProgressPercent}%`
                    : sendModalState.stage === "complete"
                      ? "100%"
                      : sendModalState.stage === "error"
                        ? t("compose.interrupted")
                        : t("compose.processing")}
                </p>
              </div>

              <div
                className="mt-2.5 h-2 overflow-hidden rounded-full bg-[#e9eef6]"
                role="progressbar"
                aria-label={hasIndividualSendProgress ? t("compose.send.individualProgressAria") : submitIntent === "draft" ? t("compose.draft.progress") : t("compose.send.progressAria")}
                aria-valuemin={0}
                aria-valuemax={100}
                aria-valuenow={
                  sendModalState.stage === "sending" && !hasIndividualSendProgress
                    ? undefined
                    : sendProgressPercent
                }
              >
                <div
                  className={`h-full rounded-full transition-[width] duration-300 ease-out ${
                    sendModalState.stage === "error"
                      ? "bg-[#d24747]"
                      : sendModalState.stage === "sending" && !hasIndividualSendProgress
                        ? "animate-pulse bg-[#2f6df6]"
                        : "bg-[#2f6df6]"
                  }`}
                  style={{
                    width: `${
                      sendModalState.stage === "sending" && !hasIndividualSendProgress
                        ? 38
                        : sendProgressPercent
                    }%`,
                  }}
                />
              </div>

              {sendModalState.stage === "error" ? (
                <div
                  role="alert"
                  className="mt-4 rounded-[12px] border border-[#f0d0d0] bg-[#fff8f8] px-3.5 py-3"
                >
                  <p className="text-[12px] font-semibold text-[#b93b3b]">{t("compose.error.reason")}</p>
                  <p className="mt-1 text-[13px] leading-5 text-[#7f3030]">
                    {sendModalState.errorMessage}
                  </p>
                  <p className="mt-1.5 text-[12px] leading-5 text-[#9a5b5b]">
                    {t("compose.error.keepDraft")}
                  </p>
                </div>
              ) : null}
            </div>

            {hasSendAttachments ? (
              <div className="overflow-x-auto border-t border-[#edf1f6]">
                <div className="min-w-[640px]">
                  <div className="grid grid-cols-[minmax(0,1.8fr)_120px_170px] gap-4 bg-[#f8fafc] px-5 py-2.5 text-[12px] font-semibold text-[#687891] sm:px-6">
                    <span>{t("compose.file.name")}</span>
                    <span>{t("compose.file.status")}</span>
                    <span>{t("compose.file.transfer")}</span>
                  </div>
                  <div className="max-h-[240px] overflow-y-auto">
                    {sendSnapshot.items.map((item, index) => (
                      <div
                        key={item.id}
                        className={`grid grid-cols-[minmax(0,1.8fr)_120px_170px] items-center gap-4 px-5 py-2.5 sm:px-6 ${
                          index === 0 ? "" : "border-t border-[#f0f3f7]"
                        }`}
                      >
                        <div className="flex min-w-0 items-center gap-2.5">
                          <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-[9px] bg-[#f2f6fc] text-[#6079a5]">
                            <AttachmentIcon isImage={item.kind === "inline-image"} />
                          </div>
                          <p className="truncate text-[13px] font-medium text-[#1f2d46]">
                            {item.filename}
                          </p>
                        </div>
                        <span className={`text-[13px] font-medium ${getComposeSendUploadStatusTone(item.status)}`}>
                          {getComposeSendUploadStatusLabel(item.status, t)}
                        </span>
                        <span className="text-[13px] tabular-nums text-[#5f6f8b]">
                          {formatUploadSize(item.uploadedBytes)} / {formatUploadSize(item.sizeBytes)}
                        </span>
                      </div>
                    ))}
                  </div>
                </div>
              </div>
            ) : null}
          </div>
        </div>,
        document.body,
      ) : null}

      {uploadProgress ? (
        <div className="pointer-events-none fixed inset-x-0 bottom-6 z-[180] flex justify-center px-4">
          <div className="w-full max-w-[420px] rounded-[22px] border border-[#d7e3fb] bg-[rgba(255,255,255,0.98)] px-5 py-4 shadow-[0_22px_54px_rgba(24,38,70,0.16)] backdrop-blur-[18px]">
            <p className="text-[13px] font-semibold text-[#2f6df6]">
              {getComposeUploadProgressTitle(uploadProgress.kind, t)}
            </p>
            <p className="mt-2 truncate text-[14px] font-semibold text-[#223250]">
              {uploadProgress.currentFileName}
            </p>
            <p className="mt-1 text-[13px] text-[#6f809d]">
              {t("compose.file.totalProgress", {
                total: uploadProgress.totalFiles,
                percent: uploadProgress.percent,
              })}
            </p>
            <div
              className="mt-3 h-2 overflow-hidden rounded-full bg-[#e6edf9]"
              role="progressbar"
              aria-label={getComposeUploadProgressTitle(uploadProgress.kind, t)}
              aria-valuemin={0}
              aria-valuemax={100}
              aria-valuenow={uploadProgress.percent}
            >
              <div
                className="h-full rounded-full bg-[linear-gradient(135deg,#2f6df6,#245de3)] transition-[width] duration-200"
                style={{ width: `${uploadProgress.percent}%` }}
              />
            </div>
          </div>
        </div>
      ) : null}
    </form>
  );
}
