import nodemailer, { type Transporter } from 'nodemailer';

import { env } from '@/lib/env';
import { logger } from '@/lib/logger';
import { getPortEmailConfig, type PortEmailConfig } from '@/lib/services/port-config';

/**
 * Creates and returns a new Nodemailer SMTP transporter using env defaults.
 * For port-scoped configuration use {@link createPortTransporter} instead.
 *
 * A new instance is created on each call so the factory can be used in
 * contexts where connection pooling is managed externally (e.g. per-request
 * in serverless, or once at worker startup).
 */
// Nodemailer's default `connectionTimeout` is 2 minutes and there is no
// `socketTimeout`, so a hung SMTP server would hold a BullMQ `email`
// worker concurrency slot for up to 2 min × 5 retry attempts = 10 min
// per job. With concurrency 5, all slots can be starved by a single
// flaky upstream. Explicit timeouts cap the worst case under a minute.
export const SMTP_TIMEOUTS = {
  connectionTimeout: 10_000,
  greetingTimeout: 10_000,
  socketTimeout: 30_000,
} as const;

export function createTransporter(): Transporter {
  return nodemailer.createTransport({
    host: env.SMTP_HOST,
    port: env.SMTP_PORT,
    // Implicitly secure when port is 465; STARTTLS for all other ports.
    secure: env.SMTP_PORT === 465,
    ...SMTP_TIMEOUTS,
    ...(env.SMTP_USER && env.SMTP_PASS
      ? { auth: { user: env.SMTP_USER, pass: env.SMTP_PASS } }
      : {}),
  });
}

function createTransporterFromConfig(cfg: PortEmailConfig): Transporter {
  return nodemailer.createTransport({
    host: cfg.smtpHost,
    port: cfg.smtpPort,
    secure: cfg.smtpPort === 465,
    ...SMTP_TIMEOUTS,
    ...(cfg.smtpUser && cfg.smtpPass ? { auth: { user: cfg.smtpUser, pass: cfg.smtpPass } } : {}),
  });
}

export interface EmailAttachmentRef {
  fileId: string;
  filename?: string;
}

export interface SendEmailOptions {
  to: string | string[];
  subject: string;
  html: string;
  from?: string;
  /** When provided, port-level email settings override env defaults. */
  portId?: string;
  text?: string;
  /**
   * File attachments to fetch from MinIO and attach to the message.
   * Resolution + cross-port enforcement happens via `resolveAttachments`
   * before the SMTP call.
   */
  attachments?: EmailAttachmentRef[];
}

/**
 * Resolve attachment refs to nodemailer attachment payloads. Reads each file
 * from MinIO and enforces port-isolation: an attachment that doesn't belong
 * to `portId` throws ForbiddenError. Returns an empty array when no refs
 * are provided.
 */
async function resolveAttachments(
  refs: EmailAttachmentRef[] | undefined,
  portId: string | undefined,
): Promise<Array<{ filename: string; content: Buffer; contentType?: string }>> {
  if (!refs || refs.length === 0) return [];
  const { db } = await import('@/lib/db');
  const { files } = await import('@/lib/db/schema/documents');
  const { eq } = await import('drizzle-orm');
  const { ForbiddenError, NotFoundError } = await import('@/lib/errors');
  // Pluggable storage backend (s3 OR filesystem). Direct MinIO imports
  // break the filesystem-mode deployment path documented in CLAUDE.md.
  const { getStorageBackend } = await import('@/lib/storage');
  const backend = await getStorageBackend();

  return Promise.all(
    refs.map(async (ref) => {
      const file = await db.query.files.findFirst({ where: eq(files.id, ref.fileId) });
      if (!file) throw new NotFoundError('File');
      if (portId && file.portId !== portId) {
        throw new ForbiddenError('File belongs to a different port');
      }
      const stream = await backend.get(file.storagePath);
      const chunks: Buffer[] = [];
      for await (const chunk of stream as AsyncIterable<Buffer | string>) {
        chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
      }
      return {
        filename: ref.filename ?? file.originalName,
        content: Buffer.concat(chunks),
        ...(file.mimeType ? { contentType: file.mimeType } : {}),
      };
    }),
  );
}

/**
 * Sends a single email via SMTP.
 *
 * Returns the nodemailer info object on success. Propagates errors to the
 * caller - callers in background jobs should wrap in try/catch and handle
 * retries via BullMQ.
 */
export async function sendEmail(
  to: string | string[],
  subject: string,
  html: string,
  from?: string,
  text?: string,
  portId?: string,
  attachments?: EmailAttachmentRef[],
  // M-EM02: optional CC / BCC. Mirror the same EMAIL_REDIRECT_TO scrub
  // as `to` so dev-mode redirects don't accidentally leak a CC outside
  // the safety net.
  cc?: string | string[],
  bcc?: string | string[],
  // Optional per-message Reply-To. Overrides the port's `email_reply_to`
  // setting (`cfg.replyTo`) when provided — used so client inquiry
  // confirmations reply to the public sales@/hello@ inbox, not the noreply From.
  replyTo?: string,
): Promise<nodemailer.SentMessageInfo> {
  const cfg = portId ? await getPortEmailConfig(portId) : null;
  const transporter = cfg ? createTransporterFromConfig(cfg) : createTransporter();

  const requestedTo = Array.isArray(to) ? to.join(', ') : to;
  const effectiveTo = env.EMAIL_REDIRECT_TO ?? requestedTo;
  const effectiveSubject = env.EMAIL_REDIRECT_TO
    ? `[redirected from ${requestedTo}] ${subject}`
    : subject;
  // CC/BCC dropped entirely under EMAIL_REDIRECT_TO - the redirect target
  // already gets the message; CCing additional recipients would defeat
  // the dev safety net.
  const effectiveCc = env.EMAIL_REDIRECT_TO ? undefined : cc;
  const effectiveBcc = env.EMAIL_REDIRECT_TO ? undefined : bcc;

  const fromHeader =
    from ??
    (cfg ? `${cfg.fromName} <${cfg.fromAddress}>` : undefined) ??
    env.SMTP_FROM ??
    `Port Nimara CRM <noreply@${env.SMTP_HOST}>`;

  const resolvedAttachments = await resolveAttachments(attachments, portId);
  const effectiveReplyTo = replyTo ?? cfg?.replyTo ?? undefined;

  const info = await transporter.sendMail({
    from: fromHeader,
    to: effectiveTo,
    subject: effectiveSubject,
    html,
    ...(effectiveReplyTo ? { replyTo: effectiveReplyTo } : {}),
    ...(text ? { text } : {}),
    ...(effectiveCc ? { cc: effectiveCc } : {}),
    ...(effectiveBcc ? { bcc: effectiveBcc } : {}),
    ...(resolvedAttachments.length > 0 ? { attachments: resolvedAttachments } : {}),
  });

  // When EMAIL_REDIRECT_TO is set we elevate to `warn` so the dev-only
  // safety net is visible in any logger config. Prod boot already refuses
  // when both are set (see env.ts superRefine) - this catches the dev /
  // staging window where someone left it in a .env by mistake.
  if (env.EMAIL_REDIRECT_TO) {
    logger.warn(
      { messageId: info.messageId, to: effectiveTo, originalTo: requestedTo, subject, portId },
      'Email sent (REDIRECTED via EMAIL_REDIRECT_TO - recipient overridden)',
    );
  } else {
    logger.debug(
      { messageId: info.messageId, to: effectiveTo, originalTo: requestedTo, subject, portId },
      'Email sent',
    );
  }

  return info;
}
