"use client";

import Link from "next/link";
import {
  useEffect,
  useRef,
  useState,
  type DragEvent as ReactDragEvent,
  type KeyboardEvent as ReactKeyboardEvent,
} from "react";
import { BottomToast } from "@/components/bottom-toast";
import type { MailPlanSummary } from "@/lib/mail-billing";
import type {
  AutoSendIntervalPreset,
  AutoSendRecipientDuplicateScope,
  MailAutoSendDeliveryLog,
  MailAutoSendOverview,
} from "@/lib/mail-auto-send";

type RecipientRow = {
  active: boolean;
  displayName: string;
  email: string;
  key: string;
};

type PhraseRow = {
  active: boolean;
  bodyText: string;
  key: string;
  selected: boolean;
  subjectText: string;
};

type AutoSendLogRow = {
  actionLabel: string;
  errorMessage: string | null;
  key: string;
  recipientEmail: string;
  scheduledAt: string | null;
  sentAt: string | null;
  status: "pending" | "sending" | "sent" | "failed";
  subject: string;
};

type AutoSendPanelTab = "settings" | "logs";
type AutoSaveStatus = "idle" | "saving" | "saved" | "error";
type LogPageSize = 10 | 50 | 100;
type RecipientInputMode = "single" | "bulk";

const intervalPresetOptions = [
  { label: "매일", value: "daily" },
  { label: "매주", value: "weekly" },
  { label: "매월", value: "monthly" },
  { label: "직접설정", value: "custom" },
] satisfies { label: string; value: AutoSendIntervalPreset }[];

const intervalPresetMinutes = {
  daily: 1440,
  weekly: 10_080,
  monthly: 43_200,
} satisfies Record<Exclude<AutoSendIntervalPreset, "custom">, number>;
const minuteInMilliseconds = 60_000;
const initialRunDelayMinutes = 1;
const logPageSizeOptions = [10, 50, 100] satisfies LogPageSize[];

function createKey(prefix: string) {
  return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}

function formatDateLabel(value: string | null) {
  if (!value) {
    return "아직 없음";
  }

  const date = new Date(value);

  if (Number.isNaN(date.getTime())) {
    return "아직 없음";
  }

  return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, "0")}.${String(
    date.getDate(),
  ).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(
    date.getMinutes(),
  ).padStart(2, "0")}`;
}

function formatLogDateLabel(value: string | null) {
  if (!value) {
    return "-";
  }

  return formatDateLabel(value);
}

function formatLocalDateTimeForPayload(value: string | null) {
  if (!value) {
    return null;
  }

  const date = new Date(value);

  if (Number.isNaN(date.getTime())) {
    return null;
  }

  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(
    date.getDate(),
  ).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(
    date.getMinutes(),
  ).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}`;
}

function estimateRunAt(timestamp: number | null, intervalMinutes: number) {
  if (!timestamp) {
    return null;
  }

  return new Date(timestamp + intervalMinutes * minuteInMilliseconds).toISOString();
}

function localDayKey(timestamp: number) {
  const date = new Date(timestamp);

  return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
}

function nextLocalDayStartAt(timestamp: number | null) {
  if (!timestamp) {
    return null;
  }

  const date = new Date(timestamp);

  return new Date(
    date.getFullYear(),
    date.getMonth(),
    date.getDate() + 1,
  ).toISOString();
}

function normalizeInitialRecipients(overview: MailAutoSendOverview | null) {
  return (overview?.recipients ?? []).map((recipient) => ({
    active: recipient.active,
    displayName: recipient.displayName ?? "",
    email: recipient.email,
    key: String(recipient.id),
  })) satisfies RecipientRow[];
}

function normalizeInitialPhrases(overview: MailAutoSendOverview | null) {
  return (overview?.phrases ?? []).map((phrase) => ({
    active: phrase.active,
    bodyText: phrase.bodyText,
    key: String(phrase.id),
    selected: phrase.selected,
    subjectText: phrase.subjectText,
  })) satisfies PhraseRow[];
}

function compactRowsForPayload(rows: RecipientRow[]) {
  return rows
    .map((row) => ({
      active: row.active,
      displayName: row.displayName.trim(),
      email: row.email.trim(),
    }))
    .filter((row) => row.email);
}

function compactPhrasesForPayload(rows: PhraseRow[]) {
  return rows
    .map((row) => ({
      active: row.active,
      bodyText: row.bodyText.trim(),
      selected: row.selected,
      subjectText: row.subjectText.trim(),
    }))
    .filter((row) => row.subjectText && row.bodyText);
}

function buildPendingSubjectLabel(phrases: PhraseRow[]) {
  const selected = phrases.filter((phrase) => phrase.active && phrase.selected);

  if (selected.length === 1) {
    return selected[0]?.subjectText ?? "선택된 내용";
  }

  if (selected.length > 1) {
    return `선택된 내용 ${selected.length}개 중 랜덤`;
  }

  const active = phrases.filter((phrase) => phrase.active);

  if (active.length === 1) {
    return active[0]?.subjectText ?? "저장 후 확정";
  }

  if (active.length > 1) {
    return `등록된 내용 ${active.length}개`;
  }

  return "저장 후 확정";
}

function buildAutoSendLogRows(
  recipients: RecipientRow[],
  deliveryLogs: MailAutoSendDeliveryLog[],
  phrases: PhraseRow[],
  input: {
    currentTimestamp: number | null;
    dailySendLimit: number;
    dailySentCount: number;
    firstScheduledAt: string | null;
    sendBatchSize: number;
    sendIntervalMinutes: number;
  },
) {
  const pendingSubject = buildPendingSubjectLabel(phrases);
  const activeRecipients = recipients.filter((recipient) => recipient.active);
  const pendingScheduledAtByRecipient = new Map<string, string | null>();
  const firstScheduledTimestamp = input.firstScheduledAt
    ? new Date(input.firstScheduledAt).getTime()
    : Number.NaN;

  if (!Number.isNaN(firstScheduledTimestamp)) {
    const currentTimestamp = input.currentTimestamp;
    const dailySendLimit = Math.max(1, input.dailySendLimit);
    const sendBatchSize = Math.max(1, input.sendBatchSize);
    let scheduledTimestamp = firstScheduledTimestamp;
    let dailyRemaining =
      currentTimestamp &&
      localDayKey(scheduledTimestamp) === localDayKey(currentTimestamp)
        ? Math.max(0, dailySendLimit - input.dailySentCount)
        : dailySendLimit;

    if (dailyRemaining === 0 && currentTimestamp) {
      scheduledTimestamp = new Date(
        nextLocalDayStartAt(currentTimestamp) ?? currentTimestamp,
      ).getTime();
      dailyRemaining = dailySendLimit;
    }

    let batchRemaining = Math.min(sendBatchSize, dailyRemaining);

    activeRecipients.forEach((recipient, index) => {
      pendingScheduledAtByRecipient.set(
        recipient.key,
        new Date(scheduledTimestamp).toISOString(),
      );
      batchRemaining -= 1;
      dailyRemaining -= 1;

      if (index === activeRecipients.length - 1 || batchRemaining > 0) {
        return;
      }

      if (dailyRemaining === 0) {
        scheduledTimestamp = new Date(
          nextLocalDayStartAt(scheduledTimestamp) ?? scheduledTimestamp,
        ).getTime();
        dailyRemaining = dailySendLimit;
      } else {
        const nextScheduledTimestamp =
          scheduledTimestamp + input.sendIntervalMinutes * minuteInMilliseconds;

        if (localDayKey(nextScheduledTimestamp) !== localDayKey(scheduledTimestamp)) {
          dailyRemaining = dailySendLimit;
        }

        scheduledTimestamp = nextScheduledTimestamp;
      }

      batchRemaining = Math.min(sendBatchSize, dailyRemaining);
    });
  }

  const pendingRows = activeRecipients.map((recipient) => ({
    actionLabel: "발송 대기",
    errorMessage: null,
    key: `pending-${recipient.key}`,
    recipientEmail: recipient.email,
    scheduledAt: pendingScheduledAtByRecipient.get(recipient.key) ?? null,
    sentAt: null,
    status: "pending" as const,
    subject: pendingSubject,
  }));
  const deliveryRows = deliveryLogs.map((log) => ({
    actionLabel:
      log.status === "sent"
        ? "발송 완료"
        : log.status === "failed"
          ? "발송 실패"
          : "발송 처리 중",
    errorMessage: log.errorMessage,
    key: `delivery-${log.id}`,
    recipientEmail: log.recipientEmail,
    scheduledAt: log.scheduledAt,
    sentAt: log.sentAt,
    status: log.status,
    subject: log.subject,
  }));

  return [...pendingRows, ...deliveryRows] satisfies AutoSendLogRow[];
}

function logStatusLabel(status: AutoSendLogRow["status"]) {
  if (status === "pending") return "발송대기중";
  if (status === "sending") return "발송중";
  if (status === "sent") return "발송완료";
  return "실패";
}

function logStatusClassName(status: AutoSendLogRow["status"]) {
  if (status === "sent") {
    return "bg-[#edf9ef] text-[#20733a]";
  }

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

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

  if (status === "pending") {
    return "bg-[#f4f7fb] text-[#64748b]";
  }

  return "bg-[#f4f7fb] text-[#64748b]";
}

function logRowClassName(status: AutoSendLogRow["status"]) {
  if (status === "sent") {
    return "bg-[#f8fff9]";
  }

  if (status === "failed") {
    return "bg-[#fffafa]";
  }

  if (status === "pending") {
    return "bg-[#fbfcff]";
  }

  return "bg-white";
}

function filterAutoSendLogRows(rows: AutoSendLogRow[], query: string) {
  const normalizedQuery = query.trim().toLowerCase();

  if (!normalizedQuery) {
    return rows;
  }

  return rows.filter((row) =>
    [
      row.recipientEmail,
      row.subject,
      logStatusLabel(row.status),
      row.actionLabel,
      row.errorMessage ?? "",
      formatLogDateLabel(row.scheduledAt),
      formatLogDateLabel(row.sentAt),
    ].some((value) => value.toLowerCase().includes(normalizedQuery)),
  );
}

function buildPaginationPages(currentPage: number, totalPages: number) {
  const windowSize = 5;
  const safeTotalPages = Math.max(1, totalPages);
  const start = Math.max(
    1,
    Math.min(currentPage - Math.floor(windowSize / 2), safeTotalPages - windowSize + 1),
  );
  const end = Math.min(safeTotalPages, start + windowSize - 1);

  return Array.from({ length: end - start + 1 }, (_, index) => start + index);
}

function paginationButtonClassName(active = false, disabled = false) {
  return `inline-flex h-9 min-w-9 items-center justify-center rounded-[10px] px-3 text-[13px] font-semibold transition-colors ${
    disabled
      ? "cursor-not-allowed text-[#b4bfd1]"
      : active
        ? "bg-[#2f6df6] text-white"
        : "cursor-pointer text-[#62728d] hover:bg-[#edf4ff] hover:text-[#2f6df6]"
  }`;
}

function autoSaveStatusLabel(status: AutoSaveStatus) {
  if (status === "saving") {
    return "저장 중...";
  }

  if (status === "saved") {
    return "자동 저장됨";
  }

  if (status === "error") {
    return "저장 실패";
  }

  return "변경사항은 자동 저장됩니다.";
}

function autoSaveStatusClassName(status: AutoSaveStatus) {
  if (status === "saving") {
    return "text-[#2f6df6]";
  }

  if (status === "error") {
    return "text-[#d23f3f]";
  }

  return "text-[#7384a0]";
}

function autoSendTabClassName(active: boolean) {
  return `h-9 rounded-[10px] px-3 text-[13px] font-semibold transition-colors ${
    active
      ? "bg-[#edf4ff] text-[#2f6df6]"
      : "text-[#64748b] hover:bg-[#f6f8fc] hover:text-[#2f6df6]"
  }`;
}

function splitIntervalMinutes(totalMinutes: number) {
  const normalized = Math.max(1, Math.floor(Number.isFinite(totalMinutes) ? totalMinutes : 1440));

  return {
    hours: Math.floor(normalized / 60),
    minutes: normalized % 60,
  };
}

function presetFromMinutes(totalMinutes: number): AutoSendIntervalPreset {
  if (totalMinutes === intervalPresetMinutes.daily) {
    return "daily";
  }

  if (totalMinutes === intervalPresetMinutes.weekly) {
    return "weekly";
  }

  if (totalMinutes === intervalPresetMinutes.monthly) {
    return "monthly";
  }

  return "custom";
}

function parseRecipientEntries(value: string) {
  const seen = new Set<string>();
  const entries: string[] = [];

  for (const line of value.split(/\r?\n/)) {
    const email = line.trim().toLowerCase();

    if (!email || seen.has(email)) {
      continue;
    }

    seen.add(email);
    entries.push(email);
  }

  return entries;
}

function sanitizeIntegerInput(value: string, max: number) {
  const digitsOnly = value.replace(/\D/g, "");

  if (!digitsOnly) {
    return "";
  }

  return String(Math.min(max, Number(digitsOnly)));
}

function normalizeIntervalInput(value: string, max: number) {
  const number = Number(value);

  if (!Number.isFinite(number)) {
    return 0;
  }

  return Math.min(max, Math.max(0, Math.floor(number)));
}

function normalizeBoundedInput(value: string, min: number, max: number, fallback: number) {
  const number = Number(value);

  if (!Number.isFinite(number)) {
    return fallback;
  }

  return Math.min(max, Math.max(min, Math.floor(number)));
}

function formatIntervalLabel(totalMinutes: number) {
  if (totalMinutes === intervalPresetMinutes.daily) {
    return "매일";
  }

  if (totalMinutes === intervalPresetMinutes.weekly) {
    return "매주";
  }

  if (totalMinutes === intervalPresetMinutes.monthly) {
    return "매월";
  }

  const hours = Math.floor(totalMinutes / 60);
  const minutes = totalMinutes % 60;
  const parts = [
    hours > 0 ? `${hours}시간` : "",
    minutes > 0 ? `${minutes}분` : "",
  ].filter(Boolean);

  return parts.length > 0 ? parts.join(" ") : "1분";
}

function moveRows<T extends { key: string }>(rows: T[], sourceKey: string, targetKey: string) {
  if (sourceKey === targetKey) {
    return rows;
  }

  const sourceIndex = rows.findIndex((row) => row.key === sourceKey);
  const targetIndex = rows.findIndex((row) => row.key === targetKey);

  if (sourceIndex === -1 || targetIndex === -1) {
    return rows;
  }

  const nextRows = [...rows];
  const [sourceRow] = nextRows.splice(sourceIndex, 1);

  if (!sourceRow) {
    return rows;
  }

  nextRows.splice(targetIndex, 0, sourceRow);
  return nextRows;
}

function AddInput({
  disabled,
  duplicateScope,
  label,
  onAdd,
  placeholder,
  value,
  onChange,
  onDuplicateScopeChange,
}: {
  disabled: boolean;
  duplicateScope: AutoSendRecipientDuplicateScope;
  label: string;
  onAdd: () => void;
  onChange: (value: string) => void;
  onDuplicateScopeChange: (value: AutoSendRecipientDuplicateScope) => void;
  placeholder: string;
  value: string;
}) {
  return (
    <div className="mt-4 flex flex-wrap items-center gap-2">
      <input
        aria-label={label}
        value={value}
        disabled={disabled}
        placeholder={placeholder}
        onChange={(event) => onChange(event.target.value)}
        onKeyDown={(event) => {
          if (event.key !== "Enter") {
            return;
          }

          event.preventDefault();
          onAdd();
        }}
        className="h-10 min-w-0 flex-1 rounded-[10px] border border-[#dbe5f7] bg-white px-3 text-[13px] text-[#1a2943] outline-none focus:border-[#bcd1ff] disabled:cursor-not-allowed disabled:bg-[#f5f7fb]"
      />
      <button
        type="button"
        disabled={disabled || !value.trim()}
        onClick={onAdd}
        className="h-10 shrink-0 rounded-[10px] bg-[#2f6df6] px-4 text-[13px] font-semibold text-white transition-colors hover:bg-[#245de3] disabled:cursor-not-allowed disabled:bg-[#c4d0e5]"
      >
        추가
      </button>
      <RecipientDuplicateScopeSwitch
        disabled={disabled}
        value={duplicateScope}
        onChange={onDuplicateScopeChange}
      />
    </div>
  );
}

function RecipientDuplicateScopeSwitch({
  disabled,
  onChange,
  value,
}: {
  disabled: boolean;
  onChange: (value: AutoSendRecipientDuplicateScope) => void;
  value: AutoSendRecipientDuplicateScope;
}) {
  return (
    <div className="inline-flex shrink-0 items-center gap-2">
      <span className="px-1.5 text-[11px] font-semibold text-[#7183a1]">
        중복 제거
      </span>
      <div
        aria-label="받는 사람 중복 제거 범위"
        className="relative inline-grid h-10 min-w-[170px] grid-cols-2 overflow-hidden rounded-[10px] border border-[#dbe5f7] bg-[#f7faff] p-0.5"
        role="group"
      >
        <span
          aria-hidden="true"
          className={`pointer-events-none absolute inset-y-0.5 left-0.5 w-[calc(50%-2px)] rounded-[7px] bg-white shadow-[0_1px_3px_rgba(33,74,145,0.12)] transition-transform duration-200 ease-out ${
            value === "all" ? "translate-x-full" : "translate-x-0"
          }`}
        />
        <button
          type="button"
          aria-pressed={value === "pending"}
          disabled={disabled}
          title="대기중인 받는 사람만 중복으로 확인합니다."
          onClick={() => onChange("pending")}
          className={`relative z-10 h-full rounded-[7px] px-2 text-[12px] font-semibold transition-colors duration-200 disabled:cursor-not-allowed disabled:text-[#aab6c9] ${
            value === "pending"
              ? "text-[#2f6df6]"
              : "text-[#7183a1] hover:text-[#2f6df6]"
          }`}
        >
          대기중에서만
        </button>
        <button
          type="button"
          aria-pressed={value === "all"}
          disabled={disabled}
          title="발송 완료 이력까지 포함해 중복으로 확인합니다."
          onClick={() => onChange("all")}
          className={`relative z-10 h-full rounded-[7px] px-2 text-[12px] font-semibold transition-colors duration-200 disabled:cursor-not-allowed disabled:text-[#aab6c9] ${
            value === "all"
              ? "text-[#2f6df6]"
              : "text-[#7183a1] hover:text-[#2f6df6]"
          }`}
        >
          전체
        </button>
      </div>
    </div>
  );
}

function PhraseSelectionMark({ selected }: { selected: boolean }) {
  return (
    <span
      aria-hidden="true"
      className={`flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-[5px] border transition-colors ${
        selected
          ? "border-[#2f6df6] bg-[#edf4ff] text-[#2f6df6]"
          : "border-[#dbe5f7] bg-white text-transparent"
      }`}
    >
      <svg
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        strokeWidth="2.4"
        strokeLinecap="round"
        strokeLinejoin="round"
        className="h-[12px] w-[12px]"
      >
        <path d="m5 12 4.2 4.2L19 6.8" />
      </svg>
    </span>
  );
}

function InfoIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.8"
      strokeLinecap="round"
      strokeLinejoin="round"
      className={className}
      aria-hidden="true"
    >
      <circle cx="12" cy="12" r="8" />
      <path d="M12 10.7v5" />
      <path d="M12 7.7h.01" />
    </svg>
  );
}

export function MailAutoSendPanel({
  overview,
  planSummary,
  canManageBilling,
}: {
  overview: MailAutoSendOverview | null;
  planSummary: MailPlanSummary;
  canManageBilling: boolean;
}) {
  const unlocked = planSummary.aiAssistAvailable;
  const controlsDisabled = !unlocked;
  const [activeTab, setActiveTab] = useState<AutoSendPanelTab>("settings");
  const [enabled, setEnabled] = useState(Boolean(overview?.enabled));
  const [randomEnabled, setRandomEnabled] = useState(
    overview?.randomEnabled ?? true,
  );
  const initialInterval = splitIntervalMinutes(overview?.sendIntervalMinutes ?? 1440);
  const [intervalPreset, setIntervalPreset] = useState<AutoSendIntervalPreset>(
    () =>
      overview?.sendIntervalPreset ??
      presetFromMinutes(overview?.sendIntervalMinutes ?? 1440),
  );
  const [intervalHours, setIntervalHours] = useState(() =>
    String(initialInterval.hours),
  );
  const [intervalMinutes, setIntervalMinutes] = useState(() =>
    String(initialInterval.minutes),
  );
  const [dailySendLimit, setDailySendLimit] = useState(() =>
    String(overview?.dailySendLimit ?? 30),
  );
  const [sendBatchSize, setSendBatchSize] = useState(() =>
    String(overview?.sendBatchSize ?? 1),
  );
  const [recipientInputMode, setRecipientInputMode] =
    useState<RecipientInputMode>("single");
  const [recipientDuplicateScope, setRecipientDuplicateScope] =
    useState<AutoSendRecipientDuplicateScope>(
      () => overview?.recipientDuplicateScope ?? "pending",
    );
  const [recipientInput, setRecipientInput] = useState("");
  const [contentSubjectInput, setContentSubjectInput] = useState("");
  const [contentBodyInput, setContentBodyInput] = useState("");
  const [nextRunAt, setNextRunAt] = useState<string | null>(
    () => overview?.nextRunAt ?? null,
  );
  const [currentTimestamp, setCurrentTimestamp] = useState<number | null>(null);
  const [autoSaveStatus, setAutoSaveStatus] = useState<AutoSaveStatus>("idle");
  const [localToast, setLocalToast] = useState<{
    message: string;
    tone: "error" | "success";
  } | null>(null);
  const [logSearchQuery, setLogSearchQuery] = useState("");
  const [logPageSize, setLogPageSize] = useState<LogPageSize>(10);
  const [logPage, setLogPage] = useState(1);
  const [draggingPhraseKey, setDraggingPhraseKey] = useState("");
  const [editingPhraseKey, setEditingPhraseKey] = useState("");
  const [editingPhraseSubjectInput, setEditingPhraseSubjectInput] = useState("");
  const [editingPhraseBodyInput, setEditingPhraseBodyInput] = useState("");
  const [dailyLimitTooltipOpen, setDailyLimitTooltipOpen] = useState(false);
  const [activationTooltipOpen, setActivationTooltipOpen] = useState(false);
  const [recipients, setRecipients] = useState<RecipientRow[]>(() =>
    normalizeInitialRecipients(overview),
  );
  const [phrases, setPhrases] = useState<PhraseRow[]>(() =>
    normalizeInitialPhrases(overview),
  );
  const hasMountedRef = useRef(false);
  const saveSequenceRef = useRef(0);
  const normalizedIntervalHours = normalizeIntervalInput(intervalHours, 720);
  const normalizedIntervalMinutes = normalizeIntervalInput(intervalMinutes, 59);
  const customIntervalMinutes = Math.max(
    1,
    normalizedIntervalHours * 60 + normalizedIntervalMinutes,
  );
  const sendIntervalMinutes =
    intervalPreset === "custom"
      ? customIntervalMinutes
      : intervalPresetMinutes[intervalPreset];
  const normalizedDailySendLimit = normalizeBoundedInput(dailySendLimit, 1, 30, 30);
  const normalizedSendBatchSize = normalizeBoundedInput(sendBatchSize, 1, 30, 1);
  const dailySentCount = Math.max(0, overview?.dailySentCount ?? 0);
  const dailyLimitReached = dailySentCount >= normalizedDailySendLimit;
  const pendingRecipients = recipients.filter((recipient) => recipient.active);
  const pendingRecipientCount = pendingRecipients.length;
  const pendingSubjectLabel = buildPendingSubjectLabel(phrases);
  const pendingScheduledAt = enabled
    ? nextRunAt ?? estimateRunAt(currentTimestamp, sendIntervalMinutes)
    : estimateRunAt(currentTimestamp, sendIntervalMinutes);
  const autoSendLogRows = buildAutoSendLogRows(
    recipients,
    overview?.deliveryLogs ?? [],
    phrases,
    {
      currentTimestamp,
      dailySendLimit: normalizedDailySendLimit,
      dailySentCount,
      firstScheduledAt: pendingScheduledAt,
      sendBatchSize: normalizedSendBatchSize,
      sendIntervalMinutes,
    },
  );
  const filteredAutoSendLogRows = filterAutoSendLogRows(autoSendLogRows, logSearchQuery);
  const logTotalPages = Math.max(
    1,
    Math.ceil(filteredAutoSendLogRows.length / logPageSize),
  );
  const currentLogPage = Math.min(logPage, logTotalPages);
  const logStartIndex =
    filteredAutoSendLogRows.length === 0 ? 0 : (currentLogPage - 1) * logPageSize;
  const logEndIndex = Math.min(
    logStartIndex + logPageSize,
    filteredAutoSendLogRows.length,
  );
  const pagedAutoSendLogRows = filteredAutoSendLogRows.slice(
    logStartIndex,
    logEndIndex,
  );
  const logPageNumbers = buildPaginationPages(currentLogPage, logTotalPages);

  useEffect(() => {
    if (!localToast) {
      return;
    }

    const timeoutId = window.setTimeout(() => setLocalToast(null), 2800);

    return () => window.clearTimeout(timeoutId);
  }, [localToast]);

  useEffect(() => {
    const initialTimeoutId = window.setTimeout(() => {
      setCurrentTimestamp(Date.now());
    }, 0);
    const intervalId = window.setInterval(() => {
      setCurrentTimestamp(Date.now());
    }, 1000);

    return () => {
      window.clearTimeout(initialTimeoutId);
      window.clearInterval(intervalId);
    };
  }, []);

  useEffect(() => {
    if (controlsDisabled) {
      return;
    }

    if (!hasMountedRef.current) {
      hasMountedRef.current = true;
      return;
    }

    const sequence = saveSequenceRef.current + 1;
    const controller = new AbortController();
    const timeoutId = window.setTimeout(async () => {
      saveSequenceRef.current = sequence;
      setAutoSaveStatus("saving");

      try {
        const response = await fetch("/api/mailbox/auto-send/settings", {
          body: JSON.stringify({
            dailySendLimit: normalizedDailySendLimit,
            enabled,
            nextRunAt: enabled ? formatLocalDateTimeForPayload(nextRunAt) : null,
            phrases: compactPhrasesForPayload(phrases),
            randomEnabled,
            recipientDuplicateScope,
            recipients: compactRowsForPayload(recipients),
            sendBatchSize: normalizedSendBatchSize,
            sendIntervalMinutes,
            sendIntervalPreset: intervalPreset,
          }),
          headers: {
            "Content-Type": "application/json",
          },
          method: "POST",
          signal: controller.signal,
        });

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

        const savedSettings = (await response.json()) as {
          nextRunAt?: string | null;
        };

        if (saveSequenceRef.current === sequence) {
          if (
            typeof savedSettings.nextRunAt === "string" ||
            savedSettings.nextRunAt === null
          ) {
            setNextRunAt(savedSettings.nextRunAt);
          }
          setAutoSaveStatus("saved");
        }
      } catch {
        if (controller.signal.aborted) {
          return;
        }

        if (saveSequenceRef.current === sequence) {
          setAutoSaveStatus("error");
        }
      }
    }, 800);

    return () => {
      window.clearTimeout(timeoutId);
      controller.abort();
    };
  }, [
    controlsDisabled,
    enabled,
    intervalPreset,
    nextRunAt,
    normalizedDailySendLimit,
    normalizedSendBatchSize,
    phrases,
    randomEnabled,
    recipientDuplicateScope,
    recipients,
    sendIntervalMinutes,
  ]);

  const handleEnabledToggle = () => {
    if (controlsDisabled) {
      return;
    }

    const nextEnabled = !enabled;
    setNextRunAt(
      nextEnabled ? estimateRunAt(Date.now(), initialRunDelayMinutes) : null,
    );
    setEnabled(nextEnabled);
  };

  const updateQueueSchedule = ({
    nextDailySendLimit = normalizedDailySendLimit,
    nextPendingRecipientCount = pendingRecipientCount,
  }: {
    nextDailySendLimit?: number;
    nextPendingRecipientCount?: number;
  }) => {
    if (controlsDisabled || !enabled) {
      return;
    }

    if (!currentTimestamp) {
      return;
    }

    if (nextPendingRecipientCount === 0) {
      setNextRunAt(null);
      return;
    }

    if (dailySentCount >= nextDailySendLimit) {
      setNextRunAt(nextLocalDayStartAt(currentTimestamp));
      return;
    }

    const wasBlockedByDailyLimit = dailySentCount >= normalizedDailySendLimit;

    if (pendingRecipientCount === 0 || wasBlockedByDailyLimit) {
      setNextRunAt(estimateRunAt(currentTimestamp, initialRunDelayMinutes));
    }
  };

  const handleIntervalPresetChange = (nextPreset: AutoSendIntervalPreset) => {
    setIntervalPreset(nextPreset);

    if (controlsDisabled || !enabled) {
      return;
    }

    const nextIntervalMinutes =
      nextPreset === "custom"
        ? customIntervalMinutes
        : intervalPresetMinutes[nextPreset];
    setNextRunAt(estimateRunAt(Date.now(), nextIntervalMinutes));
  };

  const handleIntervalHoursChange = (value: string) => {
    const nextHours = sanitizeIntegerInput(value, 720);
    setIntervalHours(nextHours);

    if (controlsDisabled || !enabled || intervalPreset !== "custom") {
      return;
    }

    const nextIntervalMinutes = Math.max(
      1,
      normalizeIntervalInput(nextHours, 720) * 60 + normalizedIntervalMinutes,
    );
    setNextRunAt(estimateRunAt(Date.now(), nextIntervalMinutes));
  };

  const handleIntervalMinutesChange = (value: string) => {
    const nextMinutes = sanitizeIntegerInput(value, 59);
    setIntervalMinutes(nextMinutes);

    if (controlsDisabled || !enabled || intervalPreset !== "custom") {
      return;
    }

    const nextIntervalMinutes = Math.max(
      1,
      normalizedIntervalHours * 60 + normalizeIntervalInput(nextMinutes, 59),
    );
    setNextRunAt(estimateRunAt(Date.now(), nextIntervalMinutes));
  };

  const handleDailySendLimitChange = (value: string) => {
    const nextValue = sanitizeIntegerInput(value, 30);
    const nextDailySendLimit = normalizeBoundedInput(nextValue, 1, 30, 30);

    updateQueueSchedule({ nextDailySendLimit });
    setDailySendLimit(nextValue);
  };

  const addRecipients = (emails: string[]) => {
    if (controlsDisabled || emails.length === 0) {
      return false;
    }

    const existingRecipientsByEmail = new Map(
      recipients.map((row) => [row.email.toLowerCase(), row]),
    );
    const reactivatedKeys = new Set<string>();
    const newRecipients: RecipientRow[] = [];
    let duplicateCount = 0;

    for (const email of emails) {
      const existingRecipient = existingRecipientsByEmail.get(email);

      if (existingRecipient?.active || recipientDuplicateScope === "all") {
        duplicateCount += 1;
        continue;
      }

      if (existingRecipient) {
        reactivatedKeys.add(existingRecipient.key);
        continue;
      }

      newRecipients.push({
        active: true,
        displayName: "",
        email,
        key: createKey("recipient"),
      });
    }

    if (duplicateCount > 0) {
      setLocalToast({
        message:
          recipientDuplicateScope === "all"
            ? duplicateCount === 1
              ? "이미 등록된 대상이 있습니다."
              : `이미 등록된 대상 ${duplicateCount}개는 제외했습니다.`
            : duplicateCount === 1
              ? "이미 대기중인 대상이 있습니다."
              : `이미 대기중인 대상 ${duplicateCount}개는 제외했습니다.`,
        tone: "error",
      });
    }

    if (reactivatedKeys.size === 0 && newRecipients.length === 0) {
      return false;
    }

    updateQueueSchedule({
      nextPendingRecipientCount:
        pendingRecipientCount + reactivatedKeys.size + newRecipients.length,
    });

    setRecipients((current) => [
      ...current.map((row) =>
        reactivatedKeys.has(row.key) ? { ...row, active: true } : row,
      ),
      ...newRecipients,
    ]);

    return true;
  };

  const addRecipient = () => {
    const email = recipientInput.trim().toLowerCase();

    if (!email) {
      return;
    }

    if (addRecipients([email])) {
      setRecipientInput("");
    }
  };

  const addBulkRecipients = () => {
    const emails = parseRecipientEntries(recipientInput);

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

    if (addRecipients(emails)) {
      setRecipientInput("");
    }
  };

  const clearPendingRecipients = () => {
    if (controlsDisabled || pendingRecipients.length === 0) {
      return;
    }

    const pendingKeys = new Set(pendingRecipients.map((recipient) => recipient.key));

    updateQueueSchedule({ nextPendingRecipientCount: 0 });

    setRecipients((current) =>
      current.filter((row) => !pendingKeys.has(row.key)),
    );
  };

  const addRecipientFromMode = () => {
    if (recipientInputMode === "bulk") {
      addBulkRecipients();
      return;
    }

    addRecipient();
  };

  const handleRecipientInputModeChange = (mode: RecipientInputMode) => {
    setRecipientInputMode(mode);
    setRecipientInput("");
  };

  const removeRecipient = (key: string) => {
    if (controlsDisabled) {
      return;
    }

    const recipient = recipients.find((row) => row.key === key);

    updateQueueSchedule({
      nextPendingRecipientCount:
        pendingRecipientCount - (recipient?.active ? 1 : 0),
    });

    setRecipients((current) => current.filter((row) => row.key !== key));
  };

  const addPhrase = () => {
    const subjectText = contentSubjectInput.trim();
    const bodyText = contentBodyInput.trim();

    if (!subjectText || !bodyText || controlsDisabled) {
      return;
    }

    setPhrases((current) => [
      ...current,
      {
        active: true,
        bodyText,
        key: createKey("phrase"),
        selected: true,
        subjectText,
      },
    ]);
    setContentSubjectInput("");
    setContentBodyInput("");
  };

  const removePhrase = (key: string) => {
    if (controlsDisabled) {
      return;
    }

    setPhrases((current) => current.filter((row) => row.key !== key));

    if (editingPhraseKey === key) {
      setEditingPhraseKey("");
      setEditingPhraseSubjectInput("");
      setEditingPhraseBodyInput("");
    }
  };

  const togglePhraseSelection = (key: string) => {
    if (controlsDisabled) {
      return;
    }

    setPhrases((current) =>
      current.map((row) =>
        row.key === key ? { ...row, selected: !row.selected } : row,
      ),
    );
  };

  const startPhraseEdit = (phrase: PhraseRow) => {
    if (controlsDisabled) {
      return;
    }

    setEditingPhraseKey(phrase.key);
    setEditingPhraseSubjectInput(phrase.subjectText);
    setEditingPhraseBodyInput(phrase.bodyText);
  };

  const cancelPhraseEdit = () => {
    setEditingPhraseKey("");
    setEditingPhraseSubjectInput("");
    setEditingPhraseBodyInput("");
  };

  const savePhraseEdit = () => {
    const subjectText = editingPhraseSubjectInput.trim();
    const bodyText = editingPhraseBodyInput.trim();

    if (!editingPhraseKey || !subjectText || !bodyText || controlsDisabled) {
      return;
    }

    setPhrases((current) =>
      current.map((row) =>
        row.key === editingPhraseKey
          ? {
              ...row,
              bodyText,
              subjectText,
            }
          : row,
      ),
    );
    cancelPhraseEdit();
  };

  const handlePhraseDrop = (
    event: ReactDragEvent<HTMLDivElement>,
    targetKey: string,
  ) => {
    event.preventDefault();

    if (!draggingPhraseKey || controlsDisabled) {
      return;
    }

    setPhrases((current) => moveRows(current, draggingPhraseKey, targetKey));
    setDraggingPhraseKey("");
  };

  const handlePhraseKeyDown = (
    event: ReactKeyboardEvent<HTMLDivElement>,
    key: string,
  ) => {
    if (event.key === "Delete") {
      event.preventDefault();
      removePhrase(key);
      return;
    }

    if (event.key !== "Enter" && event.key !== " " && event.key !== "Spacebar") {
      return;
    }

    event.preventDefault();
    togglePhraseSelection(key);
  };

  return (
    <div className="flex min-h-full w-full min-w-0 flex-col bg-white">
      {localToast ? (
        <BottomToast notice={localToast} onClose={() => setLocalToast(null)} />
      ) : null}

      <div className="border-b border-[#edf1f6] px-3 pb-3 pt-4 sm:px-4 lg:px-5 lg:pb-3 lg:pt-6">
        <div className="flex flex-wrap items-center gap-x-2 gap-y-2 text-[15px] leading-none sm:gap-x-3 sm:text-[16px]">
          <h2 className="text-[16px] font-semibold tracking-[-0.03em] text-[#111a33] sm:text-[17px]">
            자동발송 예약
          </h2>
          <span className="text-[13px] text-[#7c879b]">
            정해둔 수신자에게 예약된 시각마다 저장한 내용을 자동으로 발송합니다.
          </span>
        </div>
      </div>

      <div className="border-b border-[#edf1f6] px-3 py-2 sm:px-4 lg:px-5">
        <div className="flex items-center gap-1.5">
          <button
            type="button"
            aria-pressed={activeTab === "settings"}
            onClick={() => setActiveTab("settings")}
            className={autoSendTabClassName(activeTab === "settings")}
          >
            발송설정
          </button>
          <button
            type="button"
            aria-pressed={activeTab === "logs"}
            onClick={() => setActiveTab("logs")}
            className={autoSendTabClassName(activeTab === "logs")}
          >
            발송로그
          </button>
        </div>
      </div>

      <div className="w-full min-w-0 flex-1">
        {activeTab === "settings" ? (
          <div className="flex min-h-full flex-col overflow-hidden px-0 py-0">
          {controlsDisabled ? (
            <div className="border-b border-[#edf1f6] px-3 py-3 sm:px-4 lg:px-5">
              <p className="text-[14px] font-semibold text-[#16233c]">
                성장 플랜에서 사용할 수 있는 자동화 기능입니다.
              </p>
              {canManageBilling ? (
                <Link
                  href="/mail?panel=setup&section=billing"
                  prefetch={false}
                  className="mt-3 inline-flex text-[13px] font-semibold text-[#2f6df6] hover:text-[#245de3]"
                >
                  결제 및 구독 열기
                </Link>
              ) : null}
            </div>
          ) : null}

          <div className="border-b border-[#edf1f6] px-3 py-3 sm:px-4 lg:px-5">
            <div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
              <div>
                <div className="flex flex-wrap items-center gap-x-2 gap-y-1.5">
                  <p className="text-[15px] font-semibold tracking-[-0.03em] text-[#16233c] sm:text-[16px]">
                    발송 상태
                  </p>
                  <span
                    className={`text-[13px] font-semibold ${
                      dailyLimitReached ? "text-[#d23f3f]" : "text-[#2f6df6]"
                    }`}
                  >
                    오늘 {dailySentCount}/{normalizedDailySendLimit}
                  </span>
                  <span className="relative inline-flex">
                    <button
                      type="button"
                      aria-label="하루 최대 발송 수 안내"
                      aria-expanded={dailyLimitTooltipOpen}
                      onClick={() => setDailyLimitTooltipOpen((current) => !current)}
                      className="inline-flex h-5 w-5 cursor-pointer items-center justify-center rounded-full text-[#8b99ae] transition-colors hover:bg-[#f1f5fb] hover:text-[#2f6df6]"
                    >
                      <InfoIcon className="h-[15px] w-[15px]" />
                    </button>
                    {dailyLimitTooltipOpen ? (
                      <span
                        role="tooltip"
                        className="absolute left-1/2 top-[calc(100%+8px)] z-20 w-[244px] -translate-x-1/2 rounded-[10px] border border-[#dfe8f8] bg-white px-3 py-2.5 text-[12px] font-normal leading-5 text-[#526784] shadow-[0_16px_36px_rgba(18,32,65,0.14)] sm:left-0 sm:translate-x-0"
                      >
                        하루 최대 발송 수는 자정에 초기화됩니다. 1회 발송 수만큼 나누어 보내되, 당일 한도를 넘겨 발송하지 않습니다.
                      </span>
                    ) : null}
                  </span>
                </div>
                <p className="mt-1.5 text-[12px] leading-5 text-[#6a7b98] sm:text-[13px]">
                  마지막 발송 {formatDateLabel(overview?.lastSentAt ?? null)} · 발송 주기{" "}
                  {formatIntervalLabel(sendIntervalMinutes)} · 1회 {normalizedSendBatchSize}개
                </p>
              </div>
              <div className="flex shrink-0 items-center gap-1.5">
                <button
                  type="button"
                  role="switch"
                  aria-checked={enabled}
                  aria-label={enabled ? "예약 활성화됨" : "예약 비활성"}
                  disabled={controlsDisabled}
                  onClick={handleEnabledToggle}
                  className={`relative inline-flex h-9 w-[66px] items-center rounded-full p-1 transition-colors duration-200 ${
                    enabled ? "bg-[#2f6df6]" : "bg-[#d9e2f1]"
                  } ${controlsDisabled ? "cursor-not-allowed opacity-60" : "cursor-pointer"}`}
                >
                  <span
                    className={`h-7 w-7 rounded-full bg-white shadow-[0_6px_16px_rgba(24,39,74,0.18)] transition-transform duration-200 ${
                      enabled ? "translate-x-[30px]" : "translate-x-0"
                    }`}
                  />
                </button>
                <span className="relative inline-flex">
                  <button
                    type="button"
                    aria-label="예약 시작 시점 안내"
                    aria-expanded={activationTooltipOpen}
                    onClick={() => setActivationTooltipOpen((current) => !current)}
                    className="inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-full text-[#8b99ae] transition-colors hover:bg-[#f1f5fb] hover:text-[#2f6df6]"
                  >
                    <InfoIcon className="h-[15px] w-[15px]" />
                  </button>
                  {activationTooltipOpen ? (
                    <span
                      role="tooltip"
                      className="absolute right-0 top-[calc(100%+8px)] z-20 w-[260px] max-w-[calc(100vw-2rem)] rounded-[10px] border border-[#dfe8f8] bg-white px-3 py-2.5 text-[12px] font-normal leading-5 text-[#526784] shadow-[0_16px_36px_rgba(18,32,65,0.14)]"
                    >
                      예약을 켜면 첫 발송은 1분 뒤에 시작됩니다. 이후에는 설정한 발송 주기로 실행되며, 주기를 바꾸면 다음 발송 예상 시각도 새 주기 기준으로 바로 변경됩니다.
                    </span>
                  ) : null}
                </span>
              </div>
            </div>
          </div>

          <div className="border-b border-[#edf1f6] px-3 py-3 sm:px-4 lg:px-5">
            <div className="grid max-w-[900px] gap-3 md:grid-cols-[minmax(170px,220px)_minmax(160px,180px)_minmax(150px,170px)_minmax(0,1fr)] md:items-start">
              <label className="block">
                <span className="mb-2 block text-[13px] font-semibold text-[#4f6284]">
                  발송 주기
                </span>
                <select
                  aria-label="발송 주기 선택"
                  value={intervalPreset}
                  disabled={controlsDisabled}
                  onChange={(event) =>
                    handleIntervalPresetChange(
                      event.target.value as AutoSendIntervalPreset,
                    )
                  }
                  className="h-10 w-full rounded-[10px] border border-[#dbe5f7] bg-white px-3 text-[13px] font-medium text-[#1a2943] outline-none focus:border-[#bcd1ff] disabled:cursor-not-allowed disabled:bg-[#f5f7fb]"
                >
                  {intervalPresetOptions.map((option) => (
                    <option key={option.value} value={option.value}>
                      {option.label}
                    </option>
                  ))}
                </select>
              </label>

              <label className="block">
                <span className="mb-2 block text-[13px] font-semibold text-[#4f6284]">
                  하루 최대 발송 수
                </span>
                <input
                  aria-label="하루 최대 발송 수"
                  inputMode="numeric"
                  value={dailySendLimit}
                  disabled={controlsDisabled}
                  onChange={(event) => handleDailySendLimitChange(event.target.value)}
                  className="h-10 w-full rounded-[10px] border border-[#dbe5f7] bg-white px-3 text-[13px] text-[#1a2943] outline-none focus:border-[#bcd1ff] disabled:cursor-not-allowed disabled:bg-[#f5f7fb]"
                />
                <p className="mt-2 text-[12px] leading-5 text-[#7b8ca8]">
                  하루 최대 30개까지 발송합니다.
                </p>
              </label>

              <label className="block">
                <span className="mb-2 block text-[13px] font-semibold text-[#4f6284]">
                  1회 발송 수
                </span>
                <input
                  aria-label="1회 발송 수"
                  inputMode="numeric"
                  value={sendBatchSize}
                  disabled={controlsDisabled}
                  onChange={(event) =>
                    setSendBatchSize(sanitizeIntegerInput(event.target.value, 30))
                  }
                  className="h-10 w-full rounded-[10px] border border-[#dbe5f7] bg-white px-3 text-[13px] text-[#1a2943] outline-none focus:border-[#bcd1ff] disabled:cursor-not-allowed disabled:bg-[#f5f7fb]"
                />
                <p className="mt-2 text-[12px] leading-5 text-[#7b8ca8]">
                  주기마다 최대 30개까지 발송합니다.
                </p>
              </label>

              {intervalPreset === "custom" ? (
                <div className="block">
                  <span className="mb-2 block text-[13px] font-semibold text-[#4f6284]">
                    직접설정
                  </span>
                  <div className="grid gap-2.5 sm:grid-cols-2">
                    <label className="relative block">
                      <input
                        aria-label="발송 주기 시간"
                        inputMode="numeric"
                        value={intervalHours}
                        onChange={(event) => handleIntervalHoursChange(event.target.value)}
                        disabled={controlsDisabled}
                        className="h-10 w-full rounded-[10px] border border-[#dbe5f7] bg-white px-3 pr-11 text-[13px] text-[#1a2943] outline-none focus:border-[#bcd1ff] disabled:cursor-not-allowed disabled:bg-[#f5f7fb]"
                      />
                      <span className="pointer-events-none absolute right-4 top-1/2 -translate-y-1/2 text-[13px] font-semibold text-[#7b8ca8]">
                        시간
                      </span>
                    </label>
                    <label className="relative block">
                      <input
                        aria-label="발송 주기 분"
                        inputMode="numeric"
                        value={intervalMinutes}
                        onChange={(event) => handleIntervalMinutesChange(event.target.value)}
                        disabled={controlsDisabled}
                        className="h-10 w-full rounded-[10px] border border-[#dbe5f7] bg-white px-3 pr-10 text-[13px] text-[#1a2943] outline-none focus:border-[#bcd1ff] disabled:cursor-not-allowed disabled:bg-[#f5f7fb]"
                      />
                      <span className="pointer-events-none absolute right-4 top-1/2 -translate-y-1/2 text-[13px] font-semibold text-[#7b8ca8]">
                        분
                      </span>
                    </label>
                  </div>
                  <p className="mt-2 text-[12px] leading-5 text-[#7b8ca8]">
                    최소 1분부터 설정됩니다.
                  </p>
                </div>
              ) : null}
            </div>
          </div>

          <div className="grid gap-5 px-3 pb-32 pt-4 sm:px-4 lg:px-5">
            <section className="max-w-[860px]">
              <div>
                <p className="text-[15px] font-semibold tracking-[-0.03em] text-[#16233c] sm:text-[16px]">
                  보낼 내용
                </p>
                <p className="mt-1 text-[12px] text-[#7b8ca8] sm:text-[13px]">
                  제목과 본문을 한 세트로 추가합니다.
                </p>
              </div>

              <label className="mt-4 flex items-center gap-2.5 text-[13px] font-semibold text-[#33445f]">
                <input
                  type="checkbox"
                  checked={randomEnabled}
                  disabled={controlsDisabled}
                  onChange={(event) => setRandomEnabled(event.target.checked)}
                  className="h-[15px] w-[15px] rounded-[4px] border-[#cfd7e4] text-[#2f6df6] focus:ring-2 focus:ring-[#d7e5ff] focus:ring-offset-0 disabled:cursor-not-allowed"
                />
                선택된 내용 중 랜덤으로 발송
              </label>

              <div className="mt-4 grid gap-2.5">
                <input
                  aria-label="발송 제목 추가"
                  value={contentSubjectInput}
                  disabled={controlsDisabled}
                  placeholder="메일 제목"
                  onChange={(event) => setContentSubjectInput(event.target.value)}
                  onKeyDown={(event) => {
                    if (event.key !== "Enter") {
                      return;
                    }

                    event.preventDefault();
                    addPhrase();
                  }}
                  className="h-10 w-full rounded-[10px] border border-[#dbe5f7] bg-white px-3 text-[13px] text-[#1a2943] outline-none focus:border-[#bcd1ff] disabled:cursor-not-allowed disabled:bg-[#f5f7fb]"
                />
                <textarea
                  aria-label="발송 본문 추가"
                  value={contentBodyInput}
                  disabled={controlsDisabled}
                  placeholder="자동 발송할 본문"
                  rows={4}
                  onChange={(event) => setContentBodyInput(event.target.value)}
                  onKeyDown={(event) => {
                    if (event.key !== "Enter" || !event.ctrlKey) {
                      return;
                    }

                    event.preventDefault();
                    addPhrase();
                  }}
                  className="h-[350px] w-full resize-y rounded-[10px] border border-[#dbe5f7] bg-white px-3 py-2.5 text-[13px] leading-[1.5] text-[#1a2943] outline-none focus:border-[#bcd1ff] disabled:cursor-not-allowed disabled:bg-[#f5f7fb]"
                />
                <div className="flex justify-end">
                  <button
                    type="button"
                    disabled={
                      controlsDisabled ||
                      !contentSubjectInput.trim() ||
                      !contentBodyInput.trim()
                    }
                    onClick={addPhrase}
                    className="h-10 shrink-0 rounded-[10px] bg-[#2f6df6] px-4 text-[13px] font-semibold text-white transition-colors hover:bg-[#245de3] disabled:cursor-not-allowed disabled:bg-[#c4d0e5]"
                  >
                    내용 추가
                  </button>
                </div>
              </div>

              <div className="mt-4 space-y-2">
                {phrases.length === 0 ? (
                  <p className="rounded-[10px] border border-dashed border-[#dbe5f7] px-3 py-4 text-center text-[13px] text-[#7b8ca8]">
                    추가된 보낼 내용이 없습니다.
                  </p>
                ) : null}
                {phrases.map((phrase, index) => {
                  const editing = editingPhraseKey === phrase.key;
                  const rowToneClassName = phrase.selected
                    ? "border-[#2f6df6] bg-[#edf4ff] shadow-[inset_0_0_0_1px_rgba(47,109,246,0.16)]"
                    : "border-[#e2e9f6] bg-white";

                  if (editing) {
                    return (
                      <div
                        key={phrase.key}
                        className={`flex items-start gap-2.5 rounded-[10px] border px-3 py-2 text-[13px] outline-none transition-all ${rowToneClassName}`}
                      >
                        <PhraseSelectionMark selected={phrase.selected} />
                        <span className="w-6 shrink-0 text-center text-[12px] font-semibold leading-10 text-[#8a99b2]">
                          {index + 1}
                        </span>
                        <div className="grid min-w-0 flex-1 gap-2">
                          <input
                            aria-label="발송 제목 수정"
                            value={editingPhraseSubjectInput}
                            disabled={controlsDisabled}
                            onChange={(event) =>
                              setEditingPhraseSubjectInput(event.target.value)
                            }
                            onKeyDown={(event) => {
                              if (event.key === "Escape") {
                                event.preventDefault();
                                cancelPhraseEdit();
                                return;
                              }

                              if (event.key !== "Enter") {
                                return;
                              }

                              event.preventDefault();
                              savePhraseEdit();
                            }}
                            className="h-10 w-full rounded-[10px] border border-[#dbe5f7] bg-white px-3 text-[13px] font-semibold text-[#1a2943] outline-none focus:border-[#bcd1ff] disabled:cursor-not-allowed disabled:bg-[#f5f7fb]"
                          />
                          <textarea
                            aria-label="발송 본문 수정"
                            value={editingPhraseBodyInput}
                            disabled={controlsDisabled}
                            onChange={(event) =>
                              setEditingPhraseBodyInput(event.target.value)
                            }
                            onKeyDown={(event) => {
                              if (event.key === "Escape") {
                                event.preventDefault();
                                cancelPhraseEdit();
                                return;
                              }

                              if (event.key !== "Enter" || !event.ctrlKey) {
                                return;
                              }

                              event.preventDefault();
                              savePhraseEdit();
                            }}
                            className="min-h-[150px] w-full resize-y rounded-[10px] border border-[#dbe5f7] bg-white px-3 py-2.5 text-[13px] leading-[1.5] text-[#1a2943] outline-none focus:border-[#bcd1ff] disabled:cursor-not-allowed disabled:bg-[#f5f7fb]"
                          />
                        </div>
                        <div className="flex shrink-0 flex-col gap-2 sm:flex-row">
                          <button
                            type="button"
                            disabled={
                              controlsDisabled ||
                              !editingPhraseSubjectInput.trim() ||
                              !editingPhraseBodyInput.trim()
                            }
                            onClick={savePhraseEdit}
                            className="h-9 rounded-[10px] bg-[#2f6df6] px-3 text-[12px] font-semibold text-white transition-colors hover:bg-[#245de3] disabled:cursor-not-allowed disabled:bg-[#c4d0e5]"
                          >
                            저장
                          </button>
                          <button
                            type="button"
                            disabled={controlsDisabled}
                            onClick={cancelPhraseEdit}
                            className="h-9 rounded-[10px] px-3 text-[12px] font-semibold text-[#7a89a3] transition-colors hover:bg-white/70 hover:text-[#2f6df6] disabled:cursor-not-allowed disabled:text-[#c0c8d6]"
                          >
                            취소
                          </button>
                        </div>
                      </div>
                    );
                  }

                  return (
                    <div
                      key={phrase.key}
                      role="button"
                      aria-pressed={phrase.selected}
                      aria-disabled={controlsDisabled}
                      draggable={!controlsDisabled}
                      tabIndex={controlsDisabled ? -1 : 0}
                      onClick={() => togglePhraseSelection(phrase.key)}
                      onDoubleClick={(event) => {
                        event.preventDefault();
                        event.stopPropagation();
                        startPhraseEdit(phrase);
                      }}
                      onDragStart={() => setDraggingPhraseKey(phrase.key)}
                      onDragOver={(event) => event.preventDefault()}
                      onDragEnd={() => setDraggingPhraseKey("")}
                      onDrop={(event) => handlePhraseDrop(event, phrase.key)}
                      onKeyDown={(event) => handlePhraseKeyDown(event, phrase.key)}
                      className={`flex min-h-10 items-center gap-2.5 rounded-[10px] border px-3 py-2 text-[13px] outline-none transition-all focus:border-[#bcd1ff] ${rowToneClassName} ${
                        phrase.selected
                          ? ""
                          : "hover:border-[#cbd9f4] hover:bg-[#f8fbff]"
                      } ${
                        controlsDisabled
                          ? "cursor-not-allowed opacity-70"
                          : "cursor-pointer active:translate-y-px"
                      }`}
                    >
                      <PhraseSelectionMark selected={phrase.selected} />
                      <span className="w-6 shrink-0 text-center text-[12px] font-semibold text-[#8a99b2]">
                        {index + 1}
                      </span>
                      <div className="min-w-0 flex-1">
                        <p className="truncate font-semibold text-[#22324d]">
                          {phrase.subjectText}
                        </p>
                        <p className="mt-0.5 truncate text-[13px] text-[#667792]">
                          {phrase.bodyText}
                        </p>
                      </div>
                      <button
                        type="button"
                        disabled={controlsDisabled}
                        onClick={(event) => {
                          event.stopPropagation();
                          removePhrase(phrase.key);
                        }}
                        className="shrink-0 text-[12px] font-semibold text-[#8c9ab1] hover:text-[#e24a4a] disabled:cursor-not-allowed disabled:text-[#c0c8d6]"
                      >
                        삭제
                      </button>
                    </div>
                  );
                })}
              </div>
            </section>

            <section className="max-w-[520px]">
              <div>
                <p className="text-[15px] font-semibold tracking-[-0.03em] text-[#16233c] sm:text-[16px]">
                  받는 사람
                </p>
                <p className="mt-1 text-[12px] text-[#7b8ca8] sm:text-[13px]">
                  단건 또는 개행 기준 대량등록으로 발송 대기열에 추가합니다.
                </p>
              </div>

              <div className="mt-4 flex items-center gap-4 text-[13px] font-semibold text-[#4f6284]">
                <label className="inline-flex items-center gap-2">
                  <input
                    type="radio"
                    name="recipient-input-mode"
                    checked={recipientInputMode === "single"}
                    disabled={controlsDisabled}
                    onChange={() => handleRecipientInputModeChange("single")}
                    className="h-[15px] w-[15px] border-[#cfd7e4] text-[#2f6df6] focus:ring-2 focus:ring-[#d7e5ff] focus:ring-offset-0 disabled:cursor-not-allowed"
                  />
                  단건등록
                </label>
                <label className="inline-flex items-center gap-2">
                  <input
                    type="radio"
                    name="recipient-input-mode"
                    checked={recipientInputMode === "bulk"}
                    disabled={controlsDisabled}
                    onChange={() => handleRecipientInputModeChange("bulk")}
                    className="h-[15px] w-[15px] border-[#cfd7e4] text-[#2f6df6] focus:ring-2 focus:ring-[#d7e5ff] focus:ring-offset-0 disabled:cursor-not-allowed"
                  />
                  대량등록
                </label>
              </div>

              {recipientInputMode === "single" ? (
                <AddInput
                  label="받는 사람 추가"
                  value={recipientInput}
                  onChange={setRecipientInput}
                  onAdd={addRecipientFromMode}
                  duplicateScope={recipientDuplicateScope}
                  onDuplicateScopeChange={setRecipientDuplicateScope}
                  disabled={controlsDisabled}
                  placeholder="name@example.com"
                />
              ) : (
                <div className="mt-4 grid gap-2">
                  <textarea
                    aria-label="받는 사람 대량 추가"
                    value={recipientInput}
                    disabled={controlsDisabled}
                    placeholder={"name@example.com\nteam@example.com"}
                    rows={6}
                    onChange={(event) => setRecipientInput(event.target.value)}
                    className="min-h-[144px] w-full resize-y rounded-[10px] border border-[#dbe5f7] bg-white px-3 py-2.5 text-[13px] leading-[1.5] text-[#1a2943] outline-none focus:border-[#bcd1ff] disabled:cursor-not-allowed disabled:bg-[#f5f7fb]"
                  />
                  <div className="flex flex-wrap items-center justify-end gap-2">
                    <button
                      type="button"
                      disabled={controlsDisabled || !recipientInput.trim()}
                      onClick={addRecipientFromMode}
                      className="h-10 shrink-0 rounded-[10px] bg-[#2f6df6] px-4 text-[13px] font-semibold text-white transition-colors hover:bg-[#245de3] disabled:cursor-not-allowed disabled:bg-[#c4d0e5]"
                    >
                      추가
                    </button>
                    <RecipientDuplicateScopeSwitch
                      disabled={controlsDisabled}
                      value={recipientDuplicateScope}
                      onChange={setRecipientDuplicateScope}
                    />
                  </div>
                </div>
              )}

              <div className="mt-4">
                <div className="flex items-center justify-between gap-3">
                  <div className="flex min-w-0 items-center gap-1.5">
                    <p className="text-[13px] font-semibold text-[#33445f]">
                      대기중인 받는 사람
                    </p>
                    <span className="text-[12px] font-semibold text-[#2f6df6]">
                      {pendingRecipients.length}
                    </span>
                  </div>
                  <button
                    type="button"
                    disabled={controlsDisabled || pendingRecipients.length === 0}
                    onClick={clearPendingRecipients}
                    className="shrink-0 text-[12px] font-semibold text-[#8c9ab1] transition-colors hover:text-[#e24a4a] disabled:cursor-not-allowed disabled:text-[#c0c8d6]"
                  >
                    모두삭제
                  </button>
                </div>
                <div className="mt-2 space-y-2">
                  {pendingRecipients.length === 0 ? (
                    <p className="rounded-[10px] border border-dashed border-[#dbe5f7] px-3 py-4 text-center text-[13px] text-[#7b8ca8]">
                      대기중인 받는 사람이 없습니다.
                    </p>
                  ) : null}
                  {pendingRecipients.map((recipient, index) => (
                    <div
                      key={recipient.key}
                      className="flex min-h-10 items-center gap-2.5 rounded-[10px] border border-[#e2e9f6] bg-white px-3 py-2 text-[13px]"
                    >
                      <span className="w-6 shrink-0 text-center text-[12px] font-semibold text-[#8a99b2]">
                        {index + 1}
                      </span>
                      <div className="min-w-0 flex-1">
                        <p className="truncate font-semibold text-[#22324d]">
                          {recipient.email}
                        </p>
                        <p className="mt-0.5 truncate text-[12px] text-[#7b8ca8]">
                          {pendingSubjectLabel}
                        </p>
                      </div>
                      <button
                        type="button"
                        disabled={controlsDisabled}
                        onClick={() => removeRecipient(recipient.key)}
                        className="shrink-0 text-[12px] font-semibold text-[#8c9ab1] hover:text-[#e24a4a] disabled:cursor-not-allowed disabled:text-[#c0c8d6]"
                      >
                        삭제
                      </button>
                    </div>
                  ))}
                </div>
              </div>
            </section>
          </div>

          <div className="flex flex-col gap-3 border-t border-[#edf1f6] px-3 py-3 sm:px-4 lg:flex-row lg:items-center lg:justify-between lg:px-5">
            <p className="text-[13px] leading-6 text-[#7384a0]">
              설정이 바뀌면 잠시 후 자동으로 저장됩니다.
            </p>
            <p
              className={`text-[13px] font-semibold ${autoSaveStatusClassName(
                autoSaveStatus,
              )}`}
            >
              {autoSaveStatusLabel(autoSaveStatus)}
            </p>
          </div>
          </div>
        ) : (
          <div className="min-h-full px-3 pb-32 pt-4 sm:px-4 lg:px-5">
            <section className="min-w-0">
              <div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
                <div>
                  <p className="text-[15px] font-semibold tracking-[-0.03em] text-[#16233c] sm:text-[16px]">
                    발송 로그
                  </p>
                  <p className="mt-1 text-[12px] text-[#7b8ca8] sm:text-[13px]">
                    발송대기중인 대상과 실제 발송 처리 결과를 함께 표시합니다.
                  </p>
                </div>
                <div className="flex w-full flex-col gap-2 sm:flex-row sm:items-center lg:w-auto">
                  <input
                    aria-label="발송 로그 검색"
                    value={logSearchQuery}
                    placeholder="받는 사람, 제목, 결과 검색"
                    onChange={(event) => {
                      setLogSearchQuery(event.target.value);
                      setLogPage(1);
                    }}
                    className="h-10 w-full rounded-[10px] border border-[#dbe5f7] bg-white px-3 text-[13px] text-[#1a2943] outline-none focus:border-[#bcd1ff] sm:w-[280px]"
                  />
                  <select
                    aria-label="발송 로그 보기 개수"
                    value={logPageSize}
                    onChange={(event) => {
                      setLogPageSize(Number(event.target.value) as LogPageSize);
                      setLogPage(1);
                    }}
                    className="h-10 w-full rounded-[10px] border border-[#dbe5f7] bg-white px-3 text-[13px] font-medium text-[#1a2943] outline-none focus:border-[#bcd1ff] sm:w-[128px]"
                  >
                    {logPageSizeOptions.map((option) => (
                      <option key={option} value={option}>
                        {option}개씩
                      </option>
                    ))}
                  </select>
                </div>
              </div>

              <div className="mt-4 overflow-x-auto rounded-[10px] border border-[#e2e9f6] bg-white">
                <table className="w-full min-w-[920px] border-collapse text-left text-[13px]">
                  <thead className="bg-[#f8fafc] text-[12px] font-semibold text-[#70829f]">
                    <tr>
                      <th className="w-[110px] px-4 py-3">상태</th>
                      <th className="px-4 py-3">받는 사람</th>
                      <th className="px-4 py-3">제목</th>
                      <th className="w-[150px] px-4 py-3">예상 시각</th>
                      <th className="w-[150px] px-4 py-3">발송 시각</th>
                      <th className="w-[160px] px-4 py-3">결과</th>
                    </tr>
                  </thead>
                  <tbody className="divide-y divide-[#edf1f6]">
                    {filteredAutoSendLogRows.length === 0 ? (
                      <tr>
                        <td colSpan={6} className="px-4 py-8 text-center text-[#7b8ca8]">
                          {autoSendLogRows.length === 0
                            ? "아직 발송 로그가 없습니다."
                            : "검색 결과가 없습니다."}
                        </td>
                      </tr>
                    ) : null}
                    {pagedAutoSendLogRows.map((row) => (
                      <tr key={row.key} className={logRowClassName(row.status)}>
                        <td className="px-4 py-3 align-top">
                          <span
                            className={`inline-flex h-7 items-center rounded-full px-2.5 text-[12px] font-semibold ${logStatusClassName(
                              row.status,
                            )}`}
                          >
                            {logStatusLabel(row.status)}
                          </span>
                        </td>
                        <td className="max-w-[240px] px-4 py-3 align-top">
                          <p className="truncate font-semibold text-[#22324d]">
                            {row.recipientEmail}
                          </p>
                        </td>
                        <td className="max-w-[260px] px-4 py-3 align-top">
                          <p className="truncate text-[#3b4b66]">{row.subject}</p>
                        </td>
                        <td className="px-4 py-3 align-top text-[#667792]">
                          {formatLogDateLabel(row.scheduledAt)}
                        </td>
                        <td className="px-4 py-3 align-top text-[#667792]">
                          {formatLogDateLabel(row.sentAt)}
                        </td>
                        <td className="px-4 py-3 align-top">
                          <span
                            className={`block min-w-0 truncate ${
                              row.status === "failed" ? "text-[#d23f3f]" : "text-[#667792]"
                            }`}
                            title={row.errorMessage ?? row.actionLabel}
                          >
                            {row.errorMessage ?? row.actionLabel}
                          </span>
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>

              <div className="mt-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                <p className="text-[12px] text-[#7b8ca8]">
                  {filteredAutoSendLogRows.length === 0
                    ? "표시할 로그가 없습니다."
                    : `총 ${filteredAutoSendLogRows.length.toLocaleString("ko-KR")}건 중 ${(
                        logStartIndex + 1
                      ).toLocaleString("ko-KR")}-${logEndIndex.toLocaleString("ko-KR")}건`}
                </p>
                <div className="flex flex-wrap items-center gap-1.5">
                  <button
                    type="button"
                    disabled={currentLogPage <= 1}
                    onClick={() => setLogPage(Math.max(1, currentLogPage - 1))}
                    className={paginationButtonClassName(false, currentLogPage <= 1)}
                  >
                    이전
                  </button>
                  {logPageNumbers.map((pageNumber) => (
                    <button
                      key={pageNumber}
                      type="button"
                      aria-current={pageNumber === currentLogPage ? "page" : undefined}
                      onClick={() => setLogPage(pageNumber)}
                      className={paginationButtonClassName(pageNumber === currentLogPage)}
                    >
                      {pageNumber}
                    </button>
                  ))}
                  <button
                    type="button"
                    disabled={currentLogPage >= logTotalPages}
                    onClick={() => setLogPage(Math.min(logTotalPages, currentLogPage + 1))}
                    className={paginationButtonClassName(
                      false,
                      currentLogPage >= logTotalPages,
                    )}
                  >
                    다음
                  </button>
                </div>
              </div>
            </section>
          </div>
        )}
      </div>
    </div>
  );
}
