import { randomUUID } from "node:crypto";

import { getMailAuthUrlForCurrentRequest, getSessionUser } from "@/lib/auth";
import { getAppSessionUserByToken, readAppBearerToken } from "@/lib/app-auth";
import { getPublicMailTransportErrorMessage } from "@/lib/mail-error-messages";
import { createMailboxMessage } from "@/lib/official-mail";
import type { ComposeAttachmentMode } from "@/lib/mail-attachment-policy";

export const runtime = "nodejs";
const MAX_COMPOSE_REQUEST_BYTES = 64 * 1024 * 1024;
const COMPOSE_HEARTBEAT_INTERVAL_MS = 15_000;

function parseUploadIdList(value: FormDataEntryValue | null) {
  return String(value ?? "")
    .split(",")
    .map((item) => Number(item.trim()))
    .filter((item) => Number.isInteger(item) && item > 0);
}

function extractErrorDetail(message: string, prefix: string) {
  if (!message.startsWith(prefix)) {
    return "";
  }

  return message.slice(prefix.length).trim().slice(0, 240);
}

function getErrorCode(error: unknown) {
  if (!error || typeof error !== "object" || !("code" in error)) {
    return "";
  }

  return String(error.code);
}

function isRequestBodyTooLargeError(error: unknown) {
  const message = error instanceof Error ? error.message.toLowerCase() : "";

  return (
    message.includes("body exceeded") ||
    message.includes("body size") ||
    message.includes("payload too large") ||
    message.includes("request entity too large")
  );
}

function logComposeFailure(input: {
  error: unknown;
  requestId: string;
  recipientCount?: number;
}) {
  const error = input.error;
  const errorRecord = error && typeof error === "object" ? error : null;

  console.error("[mailbox-compose-send] request failed", {
    code: getErrorCode(error) || undefined,
    errno:
      errorRecord && "errno" in errorRecord ? Number(errorRecord.errno) || undefined : undefined,
    message: error instanceof Error ? error.message.slice(0, 500) : "unknown-error",
    name: error instanceof Error ? error.name : typeof error,
    recipientCount: input.recipientCount,
    requestId: input.requestId,
    sqlState:
      errorRecord && "sqlState" in errorRecord ? String(errorRecord.sqlState) : undefined,
  });
}

function buildComposeErrorRedirect(error: unknown, requestId: string) {
  if (error instanceof Error && error.message === "compose-required") {
    return "/mail?view=compose&error=compose-required";
  }

  if (error instanceof Error && error.message === "invalid-recipient") {
    return "/mail?view=compose&error=invalid-recipient";
  }

  if (error instanceof Error && error.message === "compose-subject-too-long") {
    return "/mail?view=compose&error=compose-subject-too-long";
  }
  if (error instanceof Error && error.message.startsWith("attachment-")) {
    return `/mail?view=compose&error=${encodeURIComponent(error.message)}`;
  }

  if (
    error instanceof Error &&
    (error.message === "mailbox-auth-missing" || error.message === "mailbox-auth-invalid")
  ) {
    return "/mail?view=compose&error=mailbox-auth";
  }

  if (error instanceof Error && error.message === "free-send-limit-daily") {
    return "/mail?view=compose&error=free-send-limit-daily";
  }

  if (error instanceof Error && error.message.startsWith("send-cooldown-active:")) {
    const detail = extractErrorDetail(error.message, "send-cooldown-active:");
    return `/mail?view=compose&error=send-cooldown-active${detail ? `&details=${encodeURIComponent(detail)}` : ""}`;
  }

  if (error instanceof Error && error.message.startsWith("mailbox-smtp-failed:")) {
    const detail = getPublicMailTransportErrorMessage(error.message, "smtp");
    return `/mail?view=compose&error=smtp-failed${detail ? `&details=${encodeURIComponent(detail)}` : ""}`;
  }

  if (error instanceof Error && error.message.startsWith("mailbox-imap-failed:")) {
    const detail = getPublicMailTransportErrorMessage(error.message, "imap");
    return `/mail?view=compose&error=imap-failed${detail ? `&details=${encodeURIComponent(detail)}` : ""}`;
  }

  const errorCode = getErrorCode(error);

  if (
    errorCode === "ER_LOCK_DEADLOCK" ||
    errorCode === "ER_LOCK_WAIT_TIMEOUT" ||
    errorCode === "ER_CON_COUNT_ERROR" ||
    errorCode === "POOL_CONNLIMIT"
  ) {
    return "/mail?view=compose&error=compose-queue-busy";
  }

  if (errorCode === "ER_DATA_TOO_LONG") {
    return "/mail?view=compose&error=compose-input-too-long";
  }

  if (errorCode === "ER_NET_PACKET_TOO_LARGE" || isRequestBodyTooLargeError(error)) {
    return "/mail?view=compose&error=compose-request-too-large";
  }

  if (
    errorCode === "ECONNREFUSED" ||
    errorCode === "ECONNRESET" ||
    errorCode === "ETIMEDOUT" ||
    errorCode === "PROTOCOL_CONNECTION_LOST"
  ) {
    return "/mail?view=compose&error=compose-service-unavailable";
  }

  return `/mail?view=compose&error=compose-failed&request=${encodeURIComponent(requestId.slice(0, 8))}`;
}

export async function POST(request: Request) {
  const requestId = randomUUID();
  const user =
    (await getSessionUser()) ??
    (await getAppSessionUserByToken(readAppBearerToken(request.headers), request.headers));

  if (!user) {
    return Response.json(
      { redirectTo: await getMailAuthUrlForCurrentRequest("/login") },
      { status: 401 },
    );
  }

  // Large files must use the raw streaming upload endpoint first. Never let
  // the multipart parser buffer a 250 MiB attachment with the message body.
  const contentLength = Number(request.headers.get("content-length"));
  if (!Number.isSafeInteger(contentLength) || contentLength < 1) {
    return Response.json({ error: "content-length-required" }, { status: 411 });
  }
  if (contentLength > MAX_COMPOSE_REQUEST_BYTES) {
    return Response.json({ error: "compose-request-too-large" }, { status: 413 });
  }

  let formData: FormData;

  try {
    formData = await request.formData();
  } catch (error) {
    logComposeFailure({ error, requestId });
    return Response.json(
      {
        redirectTo: buildComposeErrorRedirect(error, requestId),
      },
      { status: isRequestBodyTooLargeError(error) ? 413 : 400 },
    );
  }
  const to = String(formData.get("to") ?? "");
  const cc = String(formData.get("cc") ?? "");
  const bcc = String(formData.get("bcc") ?? "");
  const subject = String(formData.get("subject") ?? "");
  const senderDisplayName = String(formData.get("senderDisplayName") ?? "").slice(0, 191);
  const firstAttachmentFilename = String(formData.get("firstAttachmentFilename") ?? "").slice(0, 255);
  const sourceDraftMessageIdValue = Number(formData.get("sourceDraftMessageId") ?? 0);
  const sourceDraftMessageId = Number.isSafeInteger(sourceDraftMessageIdValue) && sourceDraftMessageIdValue > 0
    ? sourceDraftMessageIdValue
    : null;
  const body = String(formData.get("body") ?? "");
  const bodyHtml = String(formData.get("bodyHtml") ?? "");
  const intent = String(formData.get("intent") ?? "send") === "draft" ? "draft" : "send";
  const sendIndividually = String(formData.get("sendIndividually") ?? "") === "1";
  const attachmentUploadIds = parseUploadIdList(formData.get("attachmentUploadIds"));
  const largeAttachmentUploadIds = parseUploadIdList(formData.get("largeAttachmentUploadIds"));
  const attachmentFiles = formData
    .getAll("attachmentFiles")
    .filter((entry): entry is File => entry instanceof File);
  const attachmentModes = String(formData.get("attachmentModes") ?? "")
    .split(",")
    .map((mode) => mode.trim());
  const inlineImageUploadIds = parseUploadIdList(formData.get("inlineImageUploadIds"));
  const encoder = new TextEncoder();

  let streamClosed = false;
  let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
  const stream = new ReadableStream({
    async start(controller) {
      const writeEvent = (event: object) => {
        if (streamClosed) return;
        try {
          controller.enqueue(encoder.encode(`${JSON.stringify(event)}\n`));
        } catch {
          streamClosed = true;
          if (heartbeatTimer) clearInterval(heartbeatTimer);
        }
      };

      // Draft IMAP APPEND and regular-attachment queueing can outlast the
      // reverse proxy's idle response timeout even after the upload succeeds.
      // Keep the NDJSON response active until a terminal event is available.
      writeEvent({ type: "heartbeat" });
      heartbeatTimer = setInterval(() => writeEvent({ type: "heartbeat" }), COMPOSE_HEARTBEAT_INTERVAL_MS);

      try {
        if (
          attachmentUploadIds.length === 0 &&
          attachmentFiles.length === 0 &&
          inlineImageUploadIds.length === 0
        ) {
          writeEvent({ stage: "sending", type: "stage" });
        }

        const result = await createMailboxMessage(user.email, {
          attachmentUploadIds,
          largeAttachmentUploadIds,
          bcc,
          body,
          bodyHtml,
          cc,
          inlineImageUploadIds,
          intent,
          onStageChange: async (stage) => {
            writeEvent({ stage, type: "stage" });
          },
          onIndividualQueueProgress: async (progress) => {
            writeEvent({ ...progress, type: "individual-progress" });
          },
          onUploadProgress: async (snapshot) => {
            writeEvent({ snapshot, type: "upload-progress" });
          },
          rawAttachmentFiles: await Promise.all(
            attachmentFiles.map(async (file, index) => ({
              buffer: Buffer.from(await file.arrayBuffer()),
              contentType: file.type || "application/octet-stream",
              filename: file.name || "attachment",
              mode: (["regular", "user_selected_large", "auto_large"].includes(attachmentModes[index])
                ? attachmentModes[index]
                : "regular") as ComposeAttachmentMode,
              sizeBytes: file.size,
            })),
          ),
          sendIndividually,
          senderDisplayName,
          sourceDraftMessageId,
          subject,
          firstAttachmentFilename,
          to,
          // SMTP delivery runs in the durable outbound worker. Waiting here can
          // outlive the proxy timeout after the message was already accepted,
          // causing a false failure and an accidental duplicate on retry.
          waitForDelivery: false,
        });

        writeEvent({
          redirectTo: result.messageId
            ? `/mail?folder=${result.folder}&message=${result.messageId}&success=${result.success}`
            : `/mail?folder=${result.folder}&success=${result.success}`,
          type: "done",
        });
      } catch (error) {
        logComposeFailure({
          error,
          recipientCount: to
            .split(/[\n,;]+/)
            .map((value) => value.trim())
            .filter(Boolean).length,
          requestId,
        });
        writeEvent({
          redirectTo: buildComposeErrorRedirect(error, requestId),
          type: "error",
        });
      } finally {
        if (heartbeatTimer) clearInterval(heartbeatTimer);
        if (!streamClosed) {
          streamClosed = true;
          controller.close();
        }
      }
    },
    cancel() {
      streamClosed = true;
      if (heartbeatTimer) clearInterval(heartbeatTimer);
    },
  });

  return new Response(stream, {
    headers: {
      "Cache-Control": "no-cache, no-transform",
      "Content-Type": "application/x-ndjson; charset=utf-8",
      "X-Accel-Buffering": "no",
    },
  });
}
