/** SMTP resolver: branch → restaurant → platform_settings → env. */
import nodemailer from 'nodemailer';
import { db } from '@server/db/drizzle';
import { sql } from 'drizzle-orm';
import { getPlatformSmtp } from './platform-settings';
import { getRestaurantSmtp } from './restaurant-settings';

import { childLogger } from '@server/logger';
import { redactEmail } from '@server/logger/redact';
const log = childLogger('svc.email.transport');

interface SmtpConfig { host: string; port: number; user: string; pass: string; from: string; secure?: boolean }

function defaultFrom(): string {
  const url = process.env.NEXT_PUBLIC_APP_URL;
  if (url) {
    try {
      const h = new URL(url).hostname;
      if (h && h !== 'localhost') return `noreply@${h}`;
    } catch { /* */ }
  }
  return 'noreply@localhost';
}

async function getBranchSmtp(branchId: string, restaurantId?: string): Promise<SmtpConfig | null> {
  try {
    const { rows } = restaurantId
      ? await db.execute(sql`SELECT smtp_host, smtp_port, smtp_user, smtp_pass, smtp_from FROM branches WHERE id = ${branchId} AND restaurant_id = ${restaurantId}`)
      : await db.execute(sql`SELECT smtp_host, smtp_port, smtp_user, smtp_pass, smtp_from FROM branches WHERE id = ${branchId}`);
    const row = rows[0] as Record<string, unknown> | undefined;
    if (!row) return null;
    const host = row.smtp_host as string | null;
    const user = row.smtp_user as string | null;
    const pass = row.smtp_pass as string | null;
    if (!host || !user || !pass) return null;
    const port = (row.smtp_port as number | null) ?? 587;
    return { host, port, user, pass, from: (row.smtp_from as string | null) || user || defaultFrom() };
  } catch { return null; }
}

function getEnvSmtp(): SmtpConfig | null {
  const host = process.env.SMTP_HOST;
  const user = process.env.SMTP_USER;
  const pass = process.env.SMTP_PASS;
  if (!host || !user || !pass) return null;
  return {
    host,
    port: parseInt(process.env.SMTP_PORT ?? '587', 10),
    user,
    pass,
    from: process.env.SMTP_FROM || user || defaultFrom(),
  };
}

export async function resolveSmtp(branchId?: string | null, restaurantId?: string | null): Promise<{ transport: nodemailer.Transporter; from: string } | null> {
  let cfg: SmtpConfig | null = null;
  if (branchId) cfg = await getBranchSmtp(branchId, restaurantId ?? undefined);
  if (!cfg && restaurantId) {
    const tenant = await getRestaurantSmtp(restaurantId);
    if (tenant) cfg = tenant;
  }
  if (!cfg) {
    const platform = await getPlatformSmtp();
    if (platform) cfg = platform;
  }
  if (!cfg) cfg = getEnvSmtp();
  if (!cfg) return null;
  const transport = nodemailer.createTransport({
    host: cfg.host,
    port: cfg.port,
    secure: cfg.secure ?? cfg.port === 465,
    auth: { user: cfg.user, pass: cfg.pass },
  });
  return { transport, from: cfg.from };
}

export interface RawAttachment {
  filename: string;
  /** Base64-encoded file content. */
  content_b64: string;
  content_type?: string;
}

export interface SendRawOptions {
  /**
   * Optional extra SMTP headers (e.g. List-Unsubscribe). Header values are
   * forwarded as-is to nodemailer — callers are responsible for shaping the
   * value correctly (RFC 8058 List-Unsubscribe-Post: 'List-Unsubscribe=One-Click').
   */
  headers?: Record<string, string>;
}

/**
 * Pre-flight SMTP credential check.
 *
 * Resolves the transport exactly the same way sendRaw does, then calls
 * nodemailer's built-in verify() which performs EHLO + AUTH without sending
 * any message. Returns normally on success; throws a descriptive Error on
 * failure so callers can surface a clear banner before any deliveries start.
 *
 * Intended to be called at campaign-launch time for email campaigns so the
 * operator sees the problem immediately rather than after the first 3 failed
 * delivery attempts.
 */
export async function verifySmtp(
  branchId?: string | null,
  restaurantId?: string | null,
): Promise<void> {
  const smtp = await resolveSmtp(branchId, restaurantId);
  if (!smtp) {
    throw new Error(
      'No SMTP transport configured — add SMTP credentials in Branch Settings or contact your administrator.',
    );
  }
  // Hard timeout so an unreachable SMTP host doesn't stall the launch
  // request indefinitely. 8 s is generous for a local/hosted relay but
  // short enough not to block the operator's UI for a perceptible time.
  const VERIFY_TIMEOUT_MS = 8_000;
  let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
  const timeoutPromise = new Promise<never>((_, reject) => {
    timeoutHandle = setTimeout(
      () => reject(new Error('SMTP connection timed out — check that the host and port are correct.')),
      VERIFY_TIMEOUT_MS,
    );
  });

  try {
    await Promise.race([smtp.transport.verify(), timeoutPromise]);
    clearTimeout(timeoutHandle);
    log.debug({ branchId, restaurantId: restaurantId ? '[redacted]' : null }, 'SMTP pre-flight OK');
  } catch (err) {
    clearTimeout(timeoutHandle);
    const e = err as { code?: string; responseCode?: number; response?: string; message?: string };
    const code = (e.code ?? '').toUpperCase();
    const rc = typeof e.responseCode === 'number' ? e.responseCode : 0;
    const resp = (e.response ?? '').toLowerCase();
    const msg = (e.message ?? String(err)).trim();

    log.warn({ code, rc, branchId }, 'SMTP pre-flight failed');

    if (
      code === 'EAUTH' ||
      rc === 535 || rc === 534 || rc === 530 ||
      resp.includes('authentication') ||
      (resp.includes('auth') && (resp.includes('failed') || resp.includes('invalid')))
    ) {
      throw new Error('SMTP authentication failed — update credentials in Branch Settings.');
    }
    throw new Error(`SMTP connection check failed: ${msg}`);
  }
}

export async function sendRaw(
  to: string,
  subject: string,
  html: string,
  branchId?: string | null,
  restaurantId?: string | null,
  attachments?: RawAttachment[],
  opts: SendRawOptions = {},
): Promise<{ messageId: string } | null> {
  const smtp = await resolveSmtp(branchId, restaurantId);
  if (!smtp) {
    log.warn({ subject, to: redactEmail(to) }, 'No SMTP configured — cannot deliver email');
    return null;
  }
  const mailAttachments = (attachments || [])
    .filter((a) => a && a.filename && a.content_b64)
    .map((a) => ({
      filename: a.filename,
      content: Buffer.from(a.content_b64, 'base64'),
      contentType: a.content_type || 'application/octet-stream',
    }));
  const mail: Parameters<typeof smtp.transport.sendMail>[0] = {
    from: smtp.from,
    to,
    subject,
    html,
    ...(mailAttachments.length > 0 ? { attachments: mailAttachments } : {}),
  };
  if (opts.headers && Object.keys(opts.headers).length > 0) {
    mail.headers = opts.headers;
  }
  const info = await smtp.transport.sendMail(mail);
  return { messageId: info.messageId };
}
