import "server-only";
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
import {
  ImapFlow,
  type AppendResponseObject,
  type ImapFlowOptions,
  type ListResponse,
  type MessageStructureObject,
} from "imapflow";
import { simpleParser, type ParsedMail } from "mailparser";
import nodemailer from "nodemailer";

import { resolveMailboxMessageReceivedAt } from "@/lib/mail-message-date";
import {
  getConfiguredMailSendingServer,
  resolveMailSendingServerForSender,
  type MailSendingServerRuntimeConfig,
} from "@/lib/mail-sending-servers";
import { createMailboxSnippet } from "@/lib/mail-snippet";

type BuiltInRemoteFolderSystemName = "inbox" | "sent" | "drafts" | "spam" | "trash";
export type RemoteFolderSystemName = BuiltInRemoteFolderSystemName | "custom";
type RemoteSyncTarget =
  | BuiltInRemoteFolderSystemName
  | {
      name: string;
      remoteName: string;
    };

type RemoteMailRuntimeConfig = {
  credentialSecret: Buffer;
  imapAllowSelfSigned: boolean;
  imapHost: string;
  imapPort: number;
  imapSecure: boolean;
  syncLimit: number;
  tlsServerName?: string;
};

export type RemoteMailboxCredentials = {
  email: string;
  password: string;
};

export type RemoteMailboxFolder = {
  name: string;
  remoteName: string;
  systemName: RemoteFolderSystemName;
  totalCount: number;
  unreadCount: number;
};

export type RemoteMailboxFolderStatusTarget = {
  name: string;
  remoteName: string | null;
  systemName: string;
};

export type RemoteMailboxFolderStatus = {
  name: string;
  remoteName: string;
  systemName: string;
  totalCount: number;
  unreadCount: number;
};

export type RemoteMailboxBackupItem = {
  receivedAt: Date;
  remoteUid: number;
  sizeBytes: number;
  subject: string;
};

export type RemoteMailboxBackupSource = RemoteMailboxBackupItem & {
  source: Buffer;
};

export type RemoteMailboxMessage = {
  attachments: RemoteMailboxAttachmentMeta[];
  attachmentMetadataLoaded: boolean;
  bodyHtml: string | null;
  bodyText: string;
  direction: "inbound" | "outbound" | "draft";
  fromAddress: string;
  fromName: string | null;
  isRead: boolean;
  messageIdHeader: string | null;
  rawSource: string;
  receivedAt: Date;
  remoteInternalDate: Date;
  remoteFlags: string[];
  remoteFolder: string;
  remoteUid: number;
  snippet: string;
  subject: string;
  toAddresses: string;
  transportResponse: string | null;
  transportStatus: "saved" | "queued" | "sent" | "failed";
  contentLoaded: boolean;
};

export type RemoteMailboxAttachmentMeta = {
  contentDisposition: "attachment" | "inline";
  contentId: string | null;
  contentType: string;
  extension: string;
  filename: string;
  index: number;
  isInline: boolean;
  isPreviewable: boolean;
  sizeBytes: number;
};

export type RemoteMailboxAttachmentPayload = RemoteMailboxAttachmentMeta & {
  content: Buffer;
};

export type RemoteMailboxMoveResult = {
  destination: string;
  remoteUid: number | null;
};

export type RemoteMailboxSyncResult = {
  contentFailures: number[];
  currentFolderMessages: RemoteMailboxMessage[];
  currentFolderState: RemoteMailboxFolder | null;
  folders: RemoteMailboxFolder[];
};

type RemoteMailboxImapClientOptions = Pick<
  ImapFlowOptions,
  "disableAutoIdle" | "emitLogs" | "logger" | "maxIdleTime" | "socketTimeout"
>;

const REMOTE_FOLDER_DEFINITIONS: Array<{
  displayName: string;
  fallbacks: string[];
  specialUse?: string;
  systemName: BuiltInRemoteFolderSystemName;
}> = [
  {
    displayName: "받은메일",
    fallbacks: ["INBOX"],
    systemName: "inbox",
  },
  {
    displayName: "보낸메일",
    fallbacks: ["Sent", "Sent Messages", "Sent Mail"],
    specialUse: "\\Sent",
    systemName: "sent",
  },
  {
    displayName: "임시보관함",
    fallbacks: ["Drafts"],
    specialUse: "\\Drafts",
    systemName: "drafts",
  },
  {
    displayName: "스팸함",
    fallbacks: ["Junk", "Spam"],
    specialUse: "\\Junk",
    systemName: "spam",
  },
  {
    displayName: "휴지통",
    fallbacks: ["Trash", "Deleted Messages"],
    specialUse: "\\Trash",
    systemName: "trash",
  },
];

function readBooleanEnv(name: string, fallback: boolean) {
  const rawValue = process.env[name];

  if (!rawValue) {
    return fallback;
  }

  switch (rawValue.trim().toLowerCase()) {
    case "1":
    case "true":
    case "yes":
    case "on":
      return true;
    case "0":
    case "false":
    case "no":
    case "off":
      return false;
    default:
      return fallback;
  }
}

function readIntegerEnv(name: string, fallback: number) {
  const rawValue = process.env[name];

  if (!rawValue) {
    return fallback;
  }

  const parsed = Number(rawValue);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}

function resolvePublicHostFromUrl(value: string | undefined) {
  if (!value) {
    return undefined;
  }

  try {
    return new URL(value).hostname;
  } catch {
    return undefined;
  }
}

function resolveRemoteMailHost(fallback: string) {
  const configuredRemoteHost = process.env.MAIL_REMOTE_HOST?.trim();

  if (configuredRemoteHost) {
    return configuredRemoteHost;
  }

  const mxHostname = process.env.MX_HOSTNAME?.trim();

  if (mxHostname) {
    return mxHostname;
  }

  const publicMailHost = resolvePublicHostFromUrl(
    process.env.NEXT_PUBLIC_MAIL_APP_URL?.trim() ?? process.env.NEXT_PUBLIC_APP_URL?.trim(),
  );

  if (publicMailHost) {
    return publicMailHost;
  }

  return fallback;
}

function resolveImapTlsServerName(defaultRemoteHost: string) {
  return (
    process.env.MAIL_TLS_SERVERNAME?.trim() ||
    process.env.MAIL_REMOTE_HOST?.trim() ||
    process.env.MAIL_IMAP_HOST?.trim() ||
    process.env.MX_HOSTNAME?.trim() ||
    defaultRemoteHost
  );
}

function getRemoteMailRuntimeConfig(): RemoteMailRuntimeConfig {
  const defaultAllowSelfSigned = readBooleanEnv("MAILCOW_ALLOW_SELF_SIGNED", true);
  const credentialSeed = process.env.MAILBOX_CREDENTIAL_SECRET ?? process.env.DB_PASSWORD ?? "official-mail";
  const defaultRemoteHost = resolveRemoteMailHost("mx1.officialsite.kr");

  return {
    credentialSecret: createHash("sha256").update(credentialSeed).digest(),
    imapAllowSelfSigned: readBooleanEnv("MAIL_IMAP_ALLOW_SELF_SIGNED", defaultAllowSelfSigned),
    imapHost: (process.env.MAIL_IMAP_HOST ?? defaultRemoteHost).trim(),
    imapPort: readIntegerEnv("MAIL_IMAP_PORT", 993),
    imapSecure: readBooleanEnv("MAIL_IMAP_SECURE", true),
    syncLimit: readIntegerEnv("MAIL_IMAP_SYNC_LIMIT", 60),
    tlsServerName: resolveImapTlsServerName(defaultRemoteHost),
  };
}

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

  if (!trimmed) {
    throw new Error("mailbox-auth-missing");
  }

  return trimmed.split(".");
}

function formatAddressList(values?: Array<{ address?: string; name?: string }> | null) {
  if (!values?.length) {
    return "";
  }

  return values
    .map((value) => {
      const address = value.address?.trim();
      const name = value.name?.trim();

      if (!address) {
        return "";
      }

      return name ? `"${name.replace(/"/g, '\\"')}" <${address}>` : address;
    })
    .filter(Boolean)
    .join(", ");
}

function formatParsedAddressField(
  value:
    | {
        text?: string;
        value?: Array<{ address?: string; name?: string }>;
      }
    | Array<{
        text?: string;
        value?: Array<{ address?: string; name?: string }>;
      }>
    | undefined,
) {
  if (!value) {
    return "";
  }

  if (Array.isArray(value)) {
    return value
      .map((entry) => entry.text?.trim() || formatAddressList(entry.value))
      .filter(Boolean)
      .join(", ");
  }

  return value.text?.trim() || formatAddressList(value.value);
}

function stripHtml(value: string) {
  return value
    .replace(/<style[\s\S]*?<\/style>/gi, " ")
    .replace(/<script[\s\S]*?<\/script>/gi, " ")
    .replace(/<[^>]+>/g, " ")
    .replace(/&nbsp;/gi, " ")
    .replace(/\s+/g, " ")
    .trim();
}

function transportStatusForFolder(systemName: RemoteFolderSystemName) {
  if (systemName === "sent") {
    return "sent" as const;
  }

  return "saved" as const;
}

function directionForFolder(systemName: RemoteFolderSystemName) {
  if (systemName === "sent") {
    return "outbound" as const;
  }

  if (systemName === "drafts") {
    return "draft" as const;
  }

  return "inbound" as const;
}

function buildMessageBodyText(text: string | undefined, html: string | undefined) {
  const normalizedText = text?.trim();

  if (normalizedText) {
    return normalizedText;
  }

  if (typeof html === "string" && html.trim()) {
    return stripHtml(html);
  }

  return "";
}

function normalizeAttachmentFilename(value: string | undefined, index: number) {
  const trimmed = value?.trim();

  if (trimmed) {
    return trimmed;
  }

  return `attachment-${index + 1}`;
}

function attachmentExtensionFromFilename(filename: string) {
  const match = /\.([a-z0-9]{1,12})$/i.exec(filename.trim());
  return match ? match[1].toLowerCase() : "";
}

function isPreviewableAttachment(contentType: string, filename: string) {
  const normalizedContentType = contentType.trim().toLowerCase();
  const extension = attachmentExtensionFromFilename(filename);

  if (
    normalizedContentType.startsWith("image/") ||
    normalizedContentType.startsWith("text/") ||
    normalizedContentType === "application/pdf"
  ) {
    return true;
  }

  return ["pdf", "png", "jpg", "jpeg", "gif", "webp", "svg", "txt", "md", "json", "csv"].includes(
    extension,
  );
}

function normalizeMalformedAttachmentSpacing(rawSource: string) {
  return rawSource.replace(
    /(Content-Transfer-Encoding:\s*base64\r?\nContent-Disposition:[^\r\n]+(?:\r?\nContent-ID:\s*<[^>]+>)?)\r?\n(?=[A-Za-z0-9+/]{20,}(?:={0,2})(?:\r?\n|$))/gi,
    "$1\r\n\r\n",
  );
}

async function parseRawSource(rawSource: string) {
  return simpleParser(
    Buffer.from(normalizeMalformedAttachmentSpacing(rawSource), "utf-8"),
    { keepCidLinks: true },
  );
}

function normalizeAttachmentContentId(value: string) {
  const trimmed = value.trim().replace(/^<+|>+$/g, "");

  try {
    return decodeURIComponent(trimmed).toLowerCase();
  } catch {
    return trimmed.toLowerCase();
  }
}

export function classifyBodyInlineAttachments<T extends RemoteMailboxAttachmentMeta>(
  attachments: T[],
  bodyHtml: string | false | null | undefined,
) {
  const referencedContentIds = new Set<string>();
  const embeddedDataSignatures = new Set<string>();

  if (typeof bodyHtml === "string") {
    for (const match of bodyHtml.matchAll(/\bcid:([^"'\s)>]+)/gi)) {
      const contentId = normalizeAttachmentContentId(match[1] ?? "");

      if (contentId) {
        referencedContentIds.add(contentId);
      }
    }

    for (const match of bodyHtml.matchAll(
      /data:([^;,"'\s]+);base64,([a-z0-9+/=\s]+)/gi,
    )) {
      const contentType = match[1]?.trim().toLowerCase();
      const base64 = match[2]?.replace(/\s/g, "") ?? "";

      if (!contentType || !base64) {
        continue;
      }

      const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
      const sizeBytes = Math.max(0, Math.floor((base64.length * 3) / 4) - padding);
      embeddedDataSignatures.add(`${contentType}:${sizeBytes}`);
    }
  }

  return attachments.map((attachment) => ({
    ...attachment,
    isInline: Boolean(
      attachment.isInline ||
        (attachment.contentId &&
          referencedContentIds.has(normalizeAttachmentContentId(attachment.contentId))) ||
        (attachment.contentDisposition === "inline" &&
          embeddedDataSignatures.has(
            `${attachment.contentType.trim().toLowerCase()}:${attachment.sizeBytes}`,
          )),
    ),
  }));
}

export async function extractAttachmentsFromRawSource(
  rawSource: string,
): Promise<RemoteMailboxAttachmentMeta[]> {
  const parsed = await parseRawSource(rawSource);

  const attachments = parsed.attachments.map((attachment, index) => {
    const filename = normalizeAttachmentFilename(attachment.filename ?? undefined, index);
    const contentType = attachment.contentType?.trim() || "application/octet-stream";

    return {
      contentDisposition: attachment.contentDisposition === "inline" ? "inline" : "attachment",
      contentId: attachment.cid?.trim() || null,
      contentType,
      extension: attachmentExtensionFromFilename(filename),
      filename,
      index,
      isInline: Boolean(attachment.related),
      isPreviewable: isPreviewableAttachment(contentType, filename),
      sizeBytes:
        typeof attachment.size === "number"
          ? attachment.size
          : Buffer.isBuffer(attachment.content)
            ? attachment.content.length
            : 0,
    } satisfies RemoteMailboxAttachmentMeta;
  });

  return classifyBodyInlineAttachments(attachments, parsed.html);
}

function buildAttachmentMetadata(
  attachments: ParsedMail["attachments"],
  bodyHtml: string | false | null | undefined,
) {
  return classifyBodyInlineAttachments(
    attachments.map((attachment, index) => {
      const filename = normalizeAttachmentFilename(
        attachment.filename ?? undefined,
        index,
      );
      const contentType =
        attachment.contentType?.trim() || "application/octet-stream";

      return {
        contentDisposition:
          attachment.contentDisposition === "inline" ? "inline" : "attachment",
        contentId: attachment.cid?.trim() || null,
        contentType,
        extension: attachmentExtensionFromFilename(filename),
        filename,
        index,
        isInline: Boolean(attachment.related),
        isPreviewable: isPreviewableAttachment(contentType, filename),
        sizeBytes:
          typeof attachment.size === "number"
            ? attachment.size
            : Buffer.isBuffer(attachment.content)
              ? attachment.content.length
              : 0,
      } satisfies RemoteMailboxAttachmentMeta;
    }),
    bodyHtml,
  );
}

function buildBodyStructureAttachmentMetadata(
  bodyStructure: MessageStructureObject | undefined,
) {
  const attachments: RemoteMailboxAttachmentMeta[] = [];

  const visit = (node: MessageStructureObject) => {
    const contentType = node.type?.trim().toLowerCase() || "application/octet-stream";
    const disposition = node.disposition?.trim().toLowerCase() || "";
    const declaredFilename =
      node.dispositionParameters?.filename?.trim() ||
      node.parameters?.name?.trim() ||
      "";
    const hasAttachmentSemantics =
      disposition === "attachment" ||
      disposition === "inline" ||
      Boolean(declaredFilename) ||
      Boolean(node.id && !["text/html", "text/plain"].includes(contentType));

    if (hasAttachmentSemantics && !contentType.startsWith("multipart/")) {
      const filename = normalizeAttachmentFilename(
        declaredFilename || undefined,
        attachments.length,
      );
      attachments.push({
        contentDisposition: disposition === "inline" ? "inline" : "attachment",
        contentId: node.id?.trim() || null,
        contentType,
        extension: attachmentExtensionFromFilename(filename),
        filename,
        index: attachments.length,
        isInline: disposition === "inline" || Boolean(node.id),
        isPreviewable: isPreviewableAttachment(contentType, filename),
        sizeBytes: Math.max(0, Number(node.size ?? 0)),
      });
      return;
    }

    for (const child of node.childNodes ?? []) visit(child);
  };

  if (bodyStructure) visit(bodyStructure);
  return attachments;
}

export async function extractAttachmentPayloadsFromRawSource(
  rawSource: string,
): Promise<RemoteMailboxAttachmentPayload[]> {
  const parsed = await parseRawSource(rawSource);

  const attachments = parsed.attachments.flatMap((attachment, index) => {
    if (!Buffer.isBuffer(attachment.content)) {
      return [];
    }

    const filename = normalizeAttachmentFilename(attachment.filename ?? undefined, index);
    const contentType = attachment.contentType?.trim() || "application/octet-stream";

    return [{
      content: attachment.content,
      contentDisposition: attachment.contentDisposition === "inline" ? "inline" : "attachment",
      contentId: attachment.cid?.trim() || null,
      contentType,
      extension: attachmentExtensionFromFilename(filename),
      filename,
      index,
      isInline: Boolean(attachment.related),
      isPreviewable: isPreviewableAttachment(contentType, filename),
      sizeBytes:
        typeof attachment.size === "number" ? attachment.size : attachment.content.length,
    } satisfies RemoteMailboxAttachmentPayload];
  });

  return classifyBodyInlineAttachments(attachments, parsed.html);
}

export async function getAttachmentPayloadFromRawSource(
  rawSource: string,
  index: number,
): Promise<RemoteMailboxAttachmentPayload | null> {
  const attachments = await extractAttachmentPayloadsFromRawSource(rawSource);
  return attachments.find((attachment) => attachment.index === index) ?? null;
}

function pickFolderEntry(mailboxes: ListResponse[], systemName: BuiltInRemoteFolderSystemName) {
  const definition = REMOTE_FOLDER_DEFINITIONS.find((item) => item.systemName === systemName);

  if (!definition) {
    return null;
  }

  const bySpecialUse =
    definition.specialUse &&
    mailboxes.find((mailbox) => mailbox.specialUse === definition.specialUse);

  if (bySpecialUse) {
    return bySpecialUse;
  }

  return (
    mailboxes.find((mailbox) =>
      definition.fallbacks.some((fallback) => mailbox.path.toLowerCase() === fallback.toLowerCase()),
    ) ?? null
  );
}

async function withImapClient<T>(
  credentials: RemoteMailboxCredentials,
  callback: (client: ImapFlow) => Promise<T>,
) {
  const client = createRemoteMailboxImapClient(credentials);

  try {
    await client.connect();
    return await callback(client);
  } catch (error) {
    const message = error instanceof Error ? error.message : "Remote IMAP request failed";
    throw new Error(`mailbox-imap-failed:${message}`);
  } finally {
    try {
      await client.logout();
    } catch {
      client.close();
    }
  }
}

export function createRemoteMailboxImapClient(
  credentials: RemoteMailboxCredentials,
  options?: RemoteMailboxImapClientOptions,
) {
  const config = getRemoteMailRuntimeConfig();

  return new ImapFlow({
    auth: {
      pass: credentials.password,
      user: credentials.email,
    },
    disableAutoIdle: options?.disableAutoIdle,
    emitLogs: options?.emitLogs,
    host: config.imapHost,
    logger: options?.logger ?? false,
    maxIdleTime: options?.maxIdleTime,
    port: config.imapPort,
    secure: config.imapSecure,
    socketTimeout: options?.socketTimeout,
    tls: {
      rejectUnauthorized: !config.imapAllowSelfSigned,
      servername: config.tlsServerName,
    },
  });
}

function normalizeMailboxFolder(mailbox: ListResponse | null, systemName: RemoteFolderSystemName) {
  const definition = REMOTE_FOLDER_DEFINITIONS.find((item) => item.systemName === systemName);

  return {
    name: definition?.displayName ?? systemName,
    remoteName: mailbox?.path ?? definition?.fallbacks[0] ?? "INBOX",
    systemName,
    totalCount: mailbox?.status?.messages ?? 0,
    unreadCount: mailbox?.status?.unseen ?? 0,
  } satisfies RemoteMailboxFolder;
}

function normalizeCustomMailboxFolder(mailbox: ListResponse | null, input: { name: string; remoteName: string }) {
  return {
    name: input.name,
    remoteName: mailbox?.path ?? input.remoteName,
    systemName: "custom",
    totalCount: mailbox?.status?.messages ?? 0,
    unreadCount: mailbox?.status?.unseen ?? 0,
  } satisfies RemoteMailboxFolder;
}

async function fetchMailboxMessages(
  client: ImapFlow,
  mailboxAddress: string,
  remoteFolder: RemoteMailboxFolder,
  range: string | number[],
  uid: boolean,
) {
  const messages: RemoteMailboxMessage[] = [];

  for await (const message of client.fetch(
    range,
    {
      envelope: true,
      flags: true,
      internalDate: true,
      source: true,
    },
    { uid },
  )) {
    const remoteUid = Number(message.uid);

    // An EXPUNGE that races a sequence fetch can yield an incomplete FETCH row.
    // Without a stable UID the cache cannot hydrate or deduplicate it later.
    if (!Number.isInteger(remoteUid) || remoteUid <= 0) {
      continue;
    }

    const sourceBuffer = message.source ?? Buffer.from("");
    const rawSource = sourceBuffer.toString("utf-8");
    const parsed = await simpleParser(sourceBuffer, { keepCidLinks: true });
    const bodyHtml = typeof parsed.html === "string" && parsed.html.trim() ? parsed.html.trim() : null;
    const bodyText = buildMessageBodyText(parsed.text ?? undefined, bodyHtml ?? undefined);
    const envelopeFrom = message.envelope?.from?.[0];
    const parsedFrom = parsed.from?.value?.[0];
    const remoteInternalDate =
      message.internalDate instanceof Date
        ? message.internalDate
        : message.internalDate
          ? new Date(message.internalDate)
          : new Date();
    const direction = directionForFolder(remoteFolder.systemName);
    const receivedAt = resolveMailboxMessageReceivedAt({
      direction,
      internalDate: remoteInternalDate,
      messageDate: parsed.date,
      rawSource,
    });
    const toAddresses = formatParsedAddressField(parsed.to) || formatAddressList(message.envelope?.to);

    messages.push({
      attachments: buildAttachmentMetadata(parsed.attachments, parsed.html),
      attachmentMetadataLoaded: true,
      bodyHtml,
      bodyText,
      direction,
      fromAddress: parsedFrom?.address?.trim() || envelopeFrom?.address?.trim() || mailboxAddress,
      fromName: parsedFrom?.name?.trim() || envelopeFrom?.name?.trim() || null,
      isRead: Boolean(message.flags?.has("\\Seen")),
      messageIdHeader: parsed.messageId?.trim() || message.envelope?.messageId?.trim() || null,
      rawSource,
      receivedAt,
      remoteInternalDate,
      remoteFlags: [...(message.flags ?? new Set<string>())].sort(),
      remoteFolder: remoteFolder.remoteName,
      remoteUid,
      snippet: createMailboxSnippet({ bodyHtml, bodyText }),
      subject: parsed.subject?.trim() || message.envelope?.subject?.trim() || "(no subject)",
      toAddresses: toAddresses || mailboxAddress,
      transportResponse:
        remoteFolder.systemName === "sent"
          ? "발송된 메일을 메일함과 동기화했습니다."
          : "메일함과 동기화한 메일입니다.",
      transportStatus: transportStatusForFolder(remoteFolder.systemName),
      contentLoaded: true,
    });
  }

  return messages.sort((left, right) => {
    const dateDiff = right.receivedAt.getTime() - left.receivedAt.getTime();
    return dateDiff !== 0 ? dateDiff : right.remoteUid - left.remoteUid;
  });
}

async function fetchMailboxMessageSnapshots(
  client: ImapFlow,
  mailboxAddress: string,
  remoteFolder: RemoteMailboxFolder,
  range: string,
  knownRemoteUids: Set<number>,
  loadMissingContent: boolean,
) {
  const cachedMessages: RemoteMailboxMessage[] = [];
  const missingRemoteUids: number[] = [];

  for await (const message of client.fetch(
    range,
    {
      bodyStructure: true,
      envelope: true,
      flags: true,
      internalDate: true,
    },
    { uid: false },
  )) {
    const remoteUid = Number(message.uid);

    if (!Number.isInteger(remoteUid) || remoteUid <= 0) {
      continue;
    }

    if (!knownRemoteUids.has(remoteUid) && loadMissingContent) {
      missingRemoteUids.push(remoteUid);
      continue;
    }

    const envelopeFrom = message.envelope?.from?.[0];
    const remoteInternalDate =
      message.internalDate instanceof Date
        ? message.internalDate
        : message.internalDate
          ? new Date(message.internalDate)
          : new Date();

    cachedMessages.push({
      attachments: buildBodyStructureAttachmentMetadata(message.bodyStructure),
      attachmentMetadataLoaded: Boolean(message.bodyStructure),
      bodyHtml: null,
      bodyText: "",
      contentLoaded: false,
      direction: directionForFolder(remoteFolder.systemName),
      fromAddress: envelopeFrom?.address?.trim() || mailboxAddress,
      fromName: envelopeFrom?.name?.trim() || null,
      isRead: Boolean(message.flags?.has("\\Seen")),
      messageIdHeader: message.envelope?.messageId?.trim() || null,
      rawSource: "",
      receivedAt: remoteInternalDate,
      remoteInternalDate,
      remoteFlags: [...(message.flags ?? new Set<string>())].sort(),
      remoteFolder: remoteFolder.remoteName,
      remoteUid,
      snippet: "",
      subject: message.envelope?.subject?.trim() || "(no subject)",
      toAddresses: formatAddressList(message.envelope?.to) || mailboxAddress,
      transportResponse:
        remoteFolder.systemName === "sent"
          ? "발송된 메일을 메일함과 동기화했습니다."
          : "메일함과 동기화한 메일입니다.",
      transportStatus: transportStatusForFolder(remoteFolder.systemName),
    });
  }

  const fetchedMessages =
    missingRemoteUids.length > 0
      ? await fetchMailboxMessages(
          client,
          mailboxAddress,
          remoteFolder,
          missingRemoteUids,
          true,
        )
      : [];

  return [...cachedMessages, ...fetchedMessages].sort((left, right) => {
    const dateDiff = right.receivedAt.getTime() - left.receivedAt.getTime();
    return dateDiff !== 0 ? dateDiff : right.remoteUid - left.remoteUid;
  });
}

async function fetchCurrentFolderMessages(
  client: ImapFlow,
  mailboxAddress: string,
  remoteFolder: RemoteMailboxFolder,
  limit: number,
  knownRemoteUids: Set<number>,
  loadMissingContent: boolean,
  fullMetadataSnapshot: boolean,
) {
  const lock = await client.getMailboxLock(remoteFolder.remoteName, { readOnly: true });

  try {
    const mailboxState = client.mailbox;
    const exists = mailboxState ? mailboxState.exists ?? 0 : 0;

    if (!exists) {
      return [] as RemoteMailboxMessage[];
    }

    const sequenceStart = fullMetadataSnapshot ? 1 : Math.max(1, exists - limit + 1);

    if (knownRemoteUids.size > 0 || !loadMissingContent) {
      return fetchMailboxMessageSnapshots(
        client,
        mailboxAddress,
        remoteFolder,
        `${sequenceStart}:${exists}`,
        knownRemoteUids,
        loadMissingContent,
      );
    }

    return fetchMailboxMessages(
      client,
      mailboxAddress,
      remoteFolder,
      `${sequenceStart}:${exists}`,
      false,
    );
  } finally {
    lock.release();
  }
}

async function ensureRemoteMailbox(client: ImapFlow, path: string) {
  try {
    await client.mailboxCreate(path);
  } catch {
    // mailboxCreate is idempotent enough for our purposes. If the server rejects
    // creation because the mailbox exists or uses a different naming policy, we
    // still attempt to append into the requested path afterwards.
  }
}

export async function createRemoteMailboxFolder(
  credentials: RemoteMailboxCredentials,
  remoteFolder: string,
) {
  const normalizedFolder = remoteFolder.trim();

  if (!normalizedFolder) {
    return;
  }

  await withImapClient(credentials, async (client) => {
    await ensureRemoteMailbox(client, normalizedFolder);
  });
}

export async function renameRemoteMailboxFolder(
  credentials: RemoteMailboxCredentials,
  remoteFolder: string,
  nextRemoteFolder: string,
) {
  const normalizedFolder = remoteFolder.trim();
  const normalizedNextFolder = nextRemoteFolder.trim();

  if (!normalizedFolder || !normalizedNextFolder || normalizedFolder === normalizedNextFolder) {
    return;
  }

  await withImapClient(credentials, async (client) => {
    await client.mailboxRename(normalizedFolder, normalizedNextFolder);
  });
}

export async function deleteRemoteMailboxFolder(
  credentials: RemoteMailboxCredentials,
  remoteFolder: string,
) {
  const normalizedFolder = remoteFolder.trim();

  if (!normalizedFolder) {
    return;
  }

  await withImapClient(credentials, async (client) => {
    await client.mailboxDelete(normalizedFolder);
  });
}

export function encryptMailboxPassword(password: string) {
  const config = getRemoteMailRuntimeConfig();
  const iv = randomBytes(12);
  const cipher = createCipheriv("aes-256-gcm", config.credentialSecret, iv);
  const encrypted = Buffer.concat([cipher.update(password, "utf8"), cipher.final()]);
  const authTag = cipher.getAuthTag();

  return [
    "v1",
    iv.toString("base64url"),
    authTag.toString("base64url"),
    encrypted.toString("base64url"),
  ].join(".");
}

export function decryptMailboxPassword(payload: string) {
  const [version, ivEncoded, authTagEncoded, encryptedEncoded] = normalizeCipherPayload(payload);

  if (version !== "v1" || !ivEncoded || !authTagEncoded || !encryptedEncoded) {
    throw new Error("mailbox-auth-invalid");
  }

  const config = getRemoteMailRuntimeConfig();
  const decipher = createDecipheriv(
    "aes-256-gcm",
    config.credentialSecret,
    Buffer.from(ivEncoded, "base64url"),
  );

  decipher.setAuthTag(Buffer.from(authTagEncoded, "base64url"));

  const decrypted = Buffer.concat([
    decipher.update(Buffer.from(encryptedEncoded, "base64url")),
    decipher.final(),
  ]);

  return decrypted.toString("utf8");
}

export async function syncRemoteMailbox(
  credentials: RemoteMailboxCredentials,
  currentFolder: RemoteSyncTarget,
  options?: {
    fullMetadataSnapshot?: boolean;
    knownRemoteUids?: number[];
    loadMissingContent?: boolean;
  },
) {
  const config = getRemoteMailRuntimeConfig();
  const knownRemoteUids = new Set(options?.knownRemoteUids ?? []);
  const snapshot = await withImapClient(credentials, async (client) => {
    const list = await client.list({
      specialUseHints: {
        drafts: "Drafts",
        junk: "Junk",
        sent: "Sent",
        trash: "Trash",
      },
      statusQuery: {
        messages: true,
        unseen: true,
      },
    });
    const folders = REMOTE_FOLDER_DEFINITIONS.map((definition) =>
      normalizeMailboxFolder(pickFolderEntry(list, definition.systemName), definition.systemName),
    );
    const remoteFolder =
      typeof currentFolder === "string"
        ? folders.find((folder) => folder.systemName === currentFolder) ??
          folders.find((folder) => folder.systemName === "inbox") ??
          folders[0]
        : normalizeCustomMailboxFolder(
            list.find((mailbox) => mailbox.path.toLowerCase() === currentFolder.remoteName.toLowerCase()) ?? null,
            currentFolder,
          );

    if (!remoteFolder) {
      return {
        contentFailures: [],
        currentFolderMessages: [],
        currentFolderState: null,
        folders,
      } satisfies RemoteMailboxSyncResult;
    }

    return {
      currentFolderMessages: await fetchCurrentFolderMessages(
        client,
        credentials.email,
        remoteFolder,
        config.syncLimit,
        knownRemoteUids,
        false,
        options?.fullMetadataSnapshot === true,
      ),
      currentFolderState: remoteFolder,
      contentFailures: [],
      folders,
    } satisfies RemoteMailboxSyncResult;
  });

  if (options?.loadMissingContent === false || !snapshot.currentFolderState) {
    return snapshot;
  }

  const hydratedByUid = new Map<number, RemoteMailboxMessage>();
  const contentFailures: number[] = [];
  const missingMessages = snapshot.currentFolderMessages.filter(
    (message) => !knownRemoteUids.has(message.remoteUid)
  );

  // Fetch each MIME source over a fresh connection. A large or malformed
  // message can no longer discard every source fetched earlier in the folder.
  for (const message of missingMessages) {
    let hydrated: RemoteMailboxMessage | null = null;

    for (let attempt = 0; attempt < 3 && !hydrated; attempt += 1) {
      try {
        hydrated = await fetchRemoteMailboxMessage(credentials, {
          remoteFolder: snapshot.currentFolderState.remoteName,
          remoteUid: message.remoteUid,
          systemName: snapshot.currentFolderState.systemName
        });
      } catch (error) {
        if (attempt === 2) {
          console.warn(
            `[mailbox-imap] source fetch failed mailbox=${credentials.email} folder=${snapshot.currentFolderState.remoteName} uid=${message.remoteUid} message=${
              error instanceof Error ? error.message : error
            }`
          );
        } else {
          await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** attempt));
        }
      }
    }

    if (hydrated) {
      hydratedByUid.set(message.remoteUid, hydrated);
    } else {
      contentFailures.push(message.remoteUid);
    }
  }

  return {
    ...snapshot,
    contentFailures,
    currentFolderMessages: snapshot.currentFolderMessages.map(
      (message) => hydratedByUid.get(message.remoteUid) ?? message
    )
  } satisfies RemoteMailboxSyncResult;
}

export async function getRemoteMailboxFolderStatuses(
  credentials: RemoteMailboxCredentials,
  targets: RemoteMailboxFolderStatusTarget[],
) {
  return withImapClient(credentials, async (client) => {
    const list = await client.list({
      specialUseHints: {
        drafts: "Drafts",
        junk: "Junk",
        sent: "Sent",
        trash: "Trash",
      },
      statusQuery: {
        messages: true,
        unseen: true,
      },
    });

    return targets.map((target) => {
      const builtIn = REMOTE_FOLDER_DEFINITIONS.some(
        (definition) => definition.systemName === target.systemName,
      );
      const mailbox = builtIn
        ? pickFolderEntry(list, target.systemName as BuiltInRemoteFolderSystemName)
        : list.find(
            (entry) =>
              entry.path.toLowerCase() ===
              (target.remoteName || target.name).trim().toLowerCase(),
          ) ??
          list.find(
            (entry) => entry.name.trim().toLowerCase() === target.name.trim().toLowerCase(),
          ) ??
          null;

      return {
        name: target.name,
        remoteName: mailbox?.path ?? target.remoteName ?? target.name,
        systemName: target.systemName,
        totalCount: mailbox?.status?.messages ?? 0,
        unreadCount: mailbox?.status?.unseen ?? 0,
      } satisfies RemoteMailboxFolderStatus;
    });
  });
}

function resolveRemoteMailboxFolderForTarget(
  mailboxes: ListResponse[],
  target: RemoteMailboxFolderStatusTarget,
) {
  const builtIn = REMOTE_FOLDER_DEFINITIONS.some(
    (definition) => definition.systemName === target.systemName,
  );
  const mailbox = builtIn
    ? pickFolderEntry(mailboxes, target.systemName as BuiltInRemoteFolderSystemName)
    : mailboxes.find(
        (entry) =>
          entry.path.toLowerCase() ===
          (target.remoteName || target.name).trim().toLowerCase(),
      ) ??
      mailboxes.find(
        (entry) => entry.name.trim().toLowerCase() === target.name.trim().toLowerCase(),
      ) ??
      null;

  if (!mailbox) {
    throw new Error("folder-not-found");
  }

  return mailbox;
}

export async function getRemoteMailboxFolderBackupItems(
  credentials: RemoteMailboxCredentials,
  target: RemoteMailboxFolderStatusTarget,
) {
  return withImapClient(credentials, async (client) => {
    const list = await client.list({
      specialUseHints: {
        drafts: "Drafts",
        junk: "Junk",
        sent: "Sent",
        trash: "Trash",
      },
    });
    const mailbox = resolveRemoteMailboxFolderForTarget(list, target);
    const lock = await client.getMailboxLock(mailbox.path, { readOnly: true });

    try {
      if (!client.mailbox || !client.mailbox.exists) {
        return [] as RemoteMailboxBackupItem[];
      }

      const items: RemoteMailboxBackupItem[] = [];

      for await (const message of client.fetch(
        "1:*",
        {
          envelope: true,
          internalDate: true,
          size: true,
        },
        { uid: false },
      )) {
        const remoteUid = Number(message.uid);

        if (!Number.isInteger(remoteUid) || remoteUid <= 0) {
          continue;
        }

        const receivedAt =
          message.internalDate instanceof Date
            ? message.internalDate
            : message.internalDate
              ? new Date(message.internalDate)
              : new Date(0);

        items.push({
          receivedAt,
          remoteUid,
          sizeBytes: Math.max(0, Number(message.size ?? 0)),
          subject: message.envelope?.subject?.trim() || "(no subject)",
        });
      }

      return items.sort((left, right) => {
        const dateDiff = left.receivedAt.getTime() - right.receivedAt.getTime();
        return dateDiff !== 0 ? dateDiff : left.remoteUid - right.remoteUid;
      });
    } finally {
      lock.release();
    }
  });
}

export async function streamRemoteMailboxFolderBackupSources(
  credentials: RemoteMailboxCredentials,
  target: RemoteMailboxFolderStatusTarget,
  remoteUids: number[],
  onSource: (message: RemoteMailboxBackupSource) => Promise<void> | void,
) {
  if (remoteUids.length === 0) {
    return 0;
  }

  return withImapClient(credentials, async (client) => {
    const list = await client.list({
      specialUseHints: {
        drafts: "Drafts",
        junk: "Junk",
        sent: "Sent",
        trash: "Trash",
      },
    });
    const mailbox = resolveRemoteMailboxFolderForTarget(list, target);
    const lock = await client.getMailboxLock(mailbox.path, { readOnly: true });
    let streamedCount = 0;

    try {
      for await (const message of client.fetch(
        remoteUids,
        {
          envelope: true,
          internalDate: true,
          size: true,
          source: true,
        },
        { uid: true },
      )) {
        const remoteUid = Number(message.uid);

        if (!Number.isInteger(remoteUid) || remoteUid <= 0 || !message.source) {
          throw new Error("mailbox-folder-backup-source-missing");
        }

        const receivedAt =
          message.internalDate instanceof Date
            ? message.internalDate
            : message.internalDate
              ? new Date(message.internalDate)
              : new Date(0);

        await onSource({
          receivedAt,
          remoteUid,
          sizeBytes: Math.max(0, Number(message.size ?? message.source.length)),
          source: message.source,
          subject: message.envelope?.subject?.trim() || "(no subject)",
        });
        streamedCount += 1;
      }
    } finally {
      lock.release();
    }

    if (streamedCount !== remoteUids.length) {
      throw new Error("mailbox-folder-backup-incomplete");
    }

    return streamedCount;
  });
}

export function getRemoteMailboxSyncLimit() {
  return getRemoteMailRuntimeConfig().syncLimit;
}

export async function fetchRemoteMailboxMessage(
  credentials: RemoteMailboxCredentials,
  input: {
    remoteFolder: string;
    remoteUid: number;
    systemName: RemoteFolderSystemName;
  },
) {
  if (!input.remoteFolder.trim() || !Number.isInteger(input.remoteUid) || input.remoteUid <= 0) {
    return null;
  }

  return withImapClient(credentials, async (client) => {
    const remoteFolder = {
      name: input.remoteFolder,
      remoteName: input.remoteFolder,
      systemName: input.systemName,
      totalCount: 0,
      unreadCount: 0,
    } satisfies RemoteMailboxFolder;
    const lock = await client.getMailboxLock(remoteFolder.remoteName, { readOnly: true });

    try {
      const messages = await fetchMailboxMessages(
        client,
        credentials.email,
        remoteFolder,
        [input.remoteUid],
        true,
      );

      return messages.find((message) => message.remoteUid === input.remoteUid) ?? null;
    } finally {
      lock.release();
    }
  });
}

export async function fetchRemoteUnreadInboxMessages(
  credentials: RemoteMailboxCredentials,
  limit = 100,
) {
  return withImapClient(credentials, async (client) => {
    const remoteFolder = normalizeMailboxFolder(null, "inbox");
    const lock = await client.getMailboxLock(remoteFolder.remoteName, { readOnly: true });

    try {
      const unreadUids = await client.search({ seen: false }, { uid: true });

      if (!unreadUids || unreadUids.length === 0) {
        return [] as RemoteMailboxMessage[];
      }

      const selectedUids = unreadUids.slice(-Math.max(1, limit));
      return fetchMailboxMessages(
        client,
        credentials.email,
        remoteFolder,
        selectedUids,
        true,
      );
    } finally {
      lock.release();
    }
  });
}

export async function markRemoteMessageAsSeen(
  credentials: RemoteMailboxCredentials,
  remoteFolder: string,
  remoteUid: number,
) {
  if (!remoteFolder || !remoteUid) {
    return false;
  }

  return withImapClient(credentials, async (client) => {
    const lock = await client.getMailboxLock(remoteFolder);

    try {
      return await client.messageFlagsAdd(remoteUid, ["\\Seen"], { uid: true });
    } finally {
      lock.release();
    }
  });
}

export async function updateRemoteMessageFlag(
  credentials: RemoteMailboxCredentials,
  remoteFolder: string,
  remoteUid: number,
  flag: string,
  enabled: boolean,
) {
  if (!remoteFolder || !remoteUid || !flag.trim()) {
    return false;
  }

  return withImapClient(credentials, async (client) => {
    const lock = await client.getMailboxLock(remoteFolder);

    try {
      return enabled
        ? await client.messageFlagsAdd(remoteUid, [flag], { uid: true })
        : await client.messageFlagsRemove(remoteUid, [flag], { uid: true });
    } finally {
      lock.release();
    }
  });
}

export async function moveRemoteMessage(
  credentials: RemoteMailboxCredentials,
  sourceRemoteFolder: string,
  remoteUid: number,
  destinationRemoteFolder: string,
): Promise<RemoteMailboxMoveResult | false> {
  if (!sourceRemoteFolder || !destinationRemoteFolder || !remoteUid) {
    return false;
  }

  return withImapClient(credentials, async (client) => {
    await ensureRemoteMailbox(client, destinationRemoteFolder);
    const lock = await client.getMailboxLock(sourceRemoteFolder);

    try {
      const result = await client.messageMove(remoteUid, destinationRemoteFolder, { uid: true });

      if (!result) {
        return false;
      }

      return {
        destination: result.destination || destinationRemoteFolder,
        remoteUid: result.uidMap?.get(remoteUid) ?? null,
      };
    } finally {
      lock.release();
    }
  });
}

export async function deleteRemoteMessages(
  credentials: RemoteMailboxCredentials,
  remoteFolder: string,
  remoteUids: number[],
) {
  const normalizedUids = [...new Set(
    remoteUids.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value > 0),
  )];

  if (!remoteFolder || normalizedUids.length === 0) {
    return false;
  }

  return withImapClient(credentials, async (client) => {
    const lock = await client.getMailboxLock(remoteFolder);

    try {
      return await client.messageDelete(normalizedUids, { uid: true });
    } finally {
      lock.release();
    }
  });
}

async function appendRemoteMessage(
  credentials: RemoteMailboxCredentials,
  remoteFolder: string,
  rawSource: string,
  flags: string[],
) {
  return withImapClient(credentials, async (client) => {
    await ensureRemoteMailbox(client, remoteFolder);
    const appendResult = (await client.append(
      remoteFolder,
      rawSource,
      flags,
      new Date(),
    )) as AppendResponseObject | false;

    return appendResult || null;
  });
}

export async function appendMessageToRemoteFolder(
  credentials: RemoteMailboxCredentials,
  remoteFolder: string,
  rawSource: string,
  flags: string[] = [],
) {
  return appendRemoteMessage(credentials, remoteFolder, rawSource, flags);
}

export async function appendDraftMessage(
  credentials: RemoteMailboxCredentials,
  rawSource: string,
  messageIdHeader?: string,
) {
  const remoteFolder =
    REMOTE_FOLDER_DEFINITIONS.find((item) => item.systemName === "drafts")?.fallbacks[0] ?? "Drafts";

  if (!messageIdHeader) return appendRemoteMessage(credentials, remoteFolder, rawSource, ["\\Draft"]);
  return withImapClient(credentials, async (client) => {
    await ensureRemoteMailbox(client, remoteFolder);
    const lock = await client.getMailboxLock(remoteFolder);
    try {
      // APPEND may have succeeded before the connection/DB update failed.
      const existing = await client.search({ header: { "message-id": messageIdHeader } }, { uid: true });
      if (existing && existing.length) {
        const status = await client.status(remoteFolder, { messages: true });
        return { destination: remoteFolder, uid: existing[0], totalCount: status.messages };
      }
      const result = await client.append(remoteFolder, rawSource, ["\\Draft"], new Date());
      if (!result || !result.uid) throw new Error("draft-append-unconfirmed");
      const status = await client.status(remoteFolder, { messages: true });
      return { ...result, totalCount: status.messages };
    } finally {
      lock.release();
    }
  });
}

export async function appendSentMessage(
  credentials: RemoteMailboxCredentials,
  rawSource: string,
) {
  const remoteFolder =
    REMOTE_FOLDER_DEFINITIONS.find((item) => item.systemName === "sent")?.fallbacks[0] ?? "Sent";

  return appendRemoteMessage(credentials, remoteFolder, rawSource, ["\\Seen"]);
}

export async function sendSmtpMessage(
  credentials: RemoteMailboxCredentials,
  input: {
    rawSource: string;
    recipients: string[];
    server?: MailSendingServerRuntimeConfig;
    serverKey?: string | null;
  },
) {
  try {
    const server =
      input.server ?? (input.serverKey === undefined
        ? await resolveMailSendingServerForSender(credentials.email)
        : await getConfiguredMailSendingServer(input.serverKey));
    const transporter = nodemailer.createTransport({
      auth: {
        pass: credentials.password,
        user: credentials.email,
      },
      host: server.host,
      port: server.port,
      requireTLS: server.requireTls,
      secure: server.secure,
      tls: {
        rejectUnauthorized: !server.allowSelfSigned,
        servername: server.tlsServerName,
      },
    });
    const info = await transporter.sendMail({
      envelope: {
        from: credentials.email,
        to: input.recipients,
      },
      raw: input.rawSource,
    });

    return {
      messageId: info.messageId,
      response: info.response ?? "SMTP accepted the message.",
    };
  } catch (error) {
    const message = error instanceof Error ? error.message : "Remote SMTP request failed";
    throw new Error(`mailbox-smtp-failed:${message}`);
  }
}
