/**
 * Per-port sales-email configuration (Phase 7 - see plan §4.9).
 *
 * Distinct from {@link getPortEmailConfig} (`port-config.ts`) which resolves
 * the **noreply** account used by automated/system emails. The sales account
 * is the human-touch outbound: brochure/berth-pdf send-outs from
 * `document-sends.service.ts`, follow-up emails composed by reps.
 *
 * Both inboxes (SMTP + IMAP) live behind the same provider account in 99% of
 * deployments; both are configured here. The IMAP half is consumed by the
 * async-bounce monitor (§14.9), out of scope for this service but exposed
 * via `getSalesImapConfig()`.
 *
 * SECURITY (§14.10): SMTP/IMAP passwords are encrypted at rest using the
 * existing `EMAIL_CREDENTIAL_KEY` symmetric key. Reps cannot read the
 * decrypted value via the API - only `manage_settings` admins can write,
 * and even they only ever see a placeholder mask on read (see the admin
 * route handler).
 */
import nodemailer, { type Transporter } from 'nodemailer';

import { env } from '@/lib/env';
import { ConflictError } from '@/lib/errors';
import { decrypt, encrypt } from '@/lib/utils/encryption';
import { SMTP_TIMEOUTS } from '@/lib/email';
import { getSetting, upsertSetting } from '@/lib/services/settings.service';
import type { AuditMeta } from '@/lib/audit';

// ─── Setting keys ────────────────────────────────────────────────────────────

export const SALES_EMAIL_KEYS = {
  fromAddress: 'sales_from_address',
  smtpHost: 'sales_smtp_host',
  smtpPort: 'sales_smtp_port',
  smtpSecure: 'sales_smtp_secure',
  smtpUser: 'sales_smtp_user',
  smtpPassEncrypted: 'sales_smtp_pass_encrypted',
  imapHost: 'sales_imap_host',
  imapPort: 'sales_imap_port',
  imapUser: 'sales_imap_user',
  imapPassEncrypted: 'sales_imap_pass_encrypted',
  authMethod: 'sales_auth_method',
  noreplyFromAddress: 'noreply_from_address',
  templateBerthPdfBody: 'email_template_send_berth_pdf_body',
  templateBrochureBody: 'email_template_send_brochure_body',
  brochureMaxUploadMb: 'brochure_max_upload_mb',
  emailAttachThresholdMb: 'email_attach_threshold_mb',
} as const;

// ─── Types ───────────────────────────────────────────────────────────────────

export interface SalesEmailConfig {
  fromAddress: string;
  smtpHost: string | null;
  smtpPort: number;
  smtpSecure: boolean;
  smtpUser: string | null;
  /** Decrypted plaintext, available only inside server-side service code. */
  smtpPass: string | null;
  authMethod: string;
  /** Whether the config is complete enough to actually send. */
  isUsable: boolean;
}

export interface SalesImapConfig {
  imapHost: string | null;
  imapPort: number;
  imapUser: string | null;
  imapPass: string | null;
  isUsable: boolean;
}

export interface SalesContentConfig {
  noreplyFromAddress: string;
  templateBerthPdfBody: string;
  templateBrochureBody: string;
  brochureMaxUploadMb: number;
  emailAttachThresholdMb: number;
}

// ─── Defaults ────────────────────────────────────────────────────────────────

const DEFAULT_BERTH_PDF_BODY = [
  'Hi {{client.fullName}},',
  '',
  'Please find attached the spec sheet for berth {{berth.mooringNumber}} at {{port.name}}.',
  '',
  'Happy to set up a call to walk through the details - let me know what works.',
  '',
  'Best,',
].join('\n');

const DEFAULT_BROCHURE_BODY = [
  'Hi {{client.fullName}},',
  '',
  'As discussed, attached is our {{port.name}} brochure with the latest information on availability, amenities, and access.',
  '',
  'Let me know if any specific berths catch your eye and I can pull together more detail.',
  '',
  'Best,',
].join('\n');

// ─── Read accessors ──────────────────────────────────────────────────────────

async function readSetting<T>(key: string, portId: string): Promise<T | null> {
  const setting = await getSetting(key, portId);
  if (!setting) return null;
  return setting.value as T;
}

function decryptOrNull(value: string | null): string | null {
  if (!value) return null;
  try {
    return decrypt(value);
  } catch {
    // If decryption fails (key rotation, corruption, etc.), return null so
    // the send fails fast rather than mis-authenticating against SMTP.
    return null;
  }
}

export async function getSalesEmailConfig(portId: string): Promise<SalesEmailConfig> {
  const [fromAddress, smtpHost, smtpPort, smtpSecure, smtpUser, smtpPassEnc, authMethod] =
    await Promise.all([
      readSetting<string>(SALES_EMAIL_KEYS.fromAddress, portId),
      readSetting<string>(SALES_EMAIL_KEYS.smtpHost, portId),
      readSetting<number>(SALES_EMAIL_KEYS.smtpPort, portId),
      readSetting<boolean>(SALES_EMAIL_KEYS.smtpSecure, portId),
      readSetting<string>(SALES_EMAIL_KEYS.smtpUser, portId),
      readSetting<string>(SALES_EMAIL_KEYS.smtpPassEncrypted, portId),
      readSetting<string>(SALES_EMAIL_KEYS.authMethod, portId),
    ]);

  const smtpPass = decryptOrNull(smtpPassEnc);

  // For `from`, fall back to the SMTP_FROM env so a brand-new port without
  // overrides still has a usable identity (for tests + dev).
  const resolvedFrom =
    fromAddress ?? env.SMTP_FROM?.replace(/^.*<(.+)>$/, '$1').trim() ?? `sales@${env.SMTP_HOST}`;

  return {
    fromAddress: resolvedFrom,
    smtpHost: smtpHost ?? env.SMTP_HOST ?? null,
    smtpPort: smtpPort ?? env.SMTP_PORT ?? 587,
    smtpSecure: smtpSecure ?? false,
    smtpUser: smtpUser ?? env.SMTP_USER ?? null,
    smtpPass: smtpPass ?? env.SMTP_PASS ?? null,
    authMethod: authMethod ?? 'app_password',
    // "Usable" means we have host + (user, pass) pair OR host + no auth
    // (some test/local SMTP doesn't auth). For prod-realistic, we require
    // creds - empty creds means we'll just bounce against the relay.
    isUsable: Boolean(
      (smtpHost ?? env.SMTP_HOST) && ((smtpUser && smtpPass) ?? (env.SMTP_USER && env.SMTP_PASS)),
    ),
  };
}

export async function getSalesImapConfig(portId: string): Promise<SalesImapConfig> {
  const [imapHost, imapPort, imapUser, imapPassEnc] = await Promise.all([
    readSetting<string>(SALES_EMAIL_KEYS.imapHost, portId),
    readSetting<number>(SALES_EMAIL_KEYS.imapPort, portId),
    readSetting<string>(SALES_EMAIL_KEYS.imapUser, portId),
    readSetting<string>(SALES_EMAIL_KEYS.imapPassEncrypted, portId),
  ]);
  const imapPass = decryptOrNull(imapPassEnc);
  return {
    imapHost: imapHost ?? null,
    imapPort: imapPort ?? 993,
    imapUser: imapUser ?? null,
    imapPass,
    isUsable: Boolean(imapHost && imapUser && imapPass),
  };
}

export async function getSalesContentConfig(portId: string): Promise<SalesContentConfig> {
  const [noreply, berthPdfBody, brochureBody, maxUpload, attachThreshold] = await Promise.all([
    readSetting<string>(SALES_EMAIL_KEYS.noreplyFromAddress, portId),
    readSetting<string>(SALES_EMAIL_KEYS.templateBerthPdfBody, portId),
    readSetting<string>(SALES_EMAIL_KEYS.templateBrochureBody, portId),
    readSetting<number>(SALES_EMAIL_KEYS.brochureMaxUploadMb, portId),
    readSetting<number>(SALES_EMAIL_KEYS.emailAttachThresholdMb, portId),
  ]);
  return {
    noreplyFromAddress:
      noreply ?? env.SMTP_FROM?.replace(/^.*<(.+)>$/, '$1').trim() ?? `noreply@${env.SMTP_HOST}`,
    templateBerthPdfBody: berthPdfBody ?? DEFAULT_BERTH_PDF_BODY,
    templateBrochureBody: brochureBody ?? DEFAULT_BROCHURE_BODY,
    brochureMaxUploadMb: maxUpload ?? 50,
    emailAttachThresholdMb: attachThreshold ?? 15,
  };
}

// ─── Write accessors ─────────────────────────────────────────────────────────

/**
 * Plain (unencrypted) input shape for the admin write path. Password fields
 * are accepted as plaintext and encrypted before storage.
 *
 * `null` semantics:
 *   - undefined => leave unchanged (don't touch the row).
 *   - null      => clear the value.
 *   - string    => set to this value.
 *
 * For password fields specifically, the empty string `""` is treated as
 * "leave unchanged" so the admin form's masked placeholder can round-trip
 * without forcing the rep to re-enter the password every save.
 */
export interface SalesEmailConfigUpdate {
  fromAddress?: string | null;
  smtpHost?: string | null;
  smtpPort?: number | null;
  smtpSecure?: boolean | null;
  smtpUser?: string | null;
  /** Plaintext; encrypted before storage. Pass `""` to leave unchanged. */
  smtpPass?: string | null;
  imapHost?: string | null;
  imapPort?: number | null;
  imapUser?: string | null;
  /** Plaintext; encrypted before storage. Pass `""` to leave unchanged. */
  imapPass?: string | null;
  authMethod?: string | null;
  noreplyFromAddress?: string | null;
  templateBerthPdfBody?: string | null;
  templateBrochureBody?: string | null;
  brochureMaxUploadMb?: number | null;
  emailAttachThresholdMb?: number | null;
}

async function writeSetting<T>(
  key: string,
  value: T | null | undefined,
  portId: string,
  meta: AuditMeta,
): Promise<void> {
  if (value === undefined) return;
  await upsertSetting(key, value, portId, meta);
}

export async function updateSalesEmailConfig(
  portId: string,
  update: SalesEmailConfigUpdate,
  meta: AuditMeta,
): Promise<void> {
  await writeSetting(SALES_EMAIL_KEYS.fromAddress, update.fromAddress, portId, meta);
  await writeSetting(SALES_EMAIL_KEYS.smtpHost, update.smtpHost, portId, meta);
  await writeSetting(SALES_EMAIL_KEYS.smtpPort, update.smtpPort, portId, meta);
  await writeSetting(SALES_EMAIL_KEYS.smtpSecure, update.smtpSecure, portId, meta);
  await writeSetting(SALES_EMAIL_KEYS.smtpUser, update.smtpUser, portId, meta);
  await writeSetting(SALES_EMAIL_KEYS.imapHost, update.imapHost, portId, meta);
  await writeSetting(SALES_EMAIL_KEYS.imapPort, update.imapPort, portId, meta);
  await writeSetting(SALES_EMAIL_KEYS.imapUser, update.imapUser, portId, meta);
  await writeSetting(SALES_EMAIL_KEYS.authMethod, update.authMethod, portId, meta);
  await writeSetting(SALES_EMAIL_KEYS.noreplyFromAddress, update.noreplyFromAddress, portId, meta);
  await writeSetting(
    SALES_EMAIL_KEYS.templateBerthPdfBody,
    update.templateBerthPdfBody,
    portId,
    meta,
  );
  await writeSetting(
    SALES_EMAIL_KEYS.templateBrochureBody,
    update.templateBrochureBody,
    portId,
    meta,
  );
  await writeSetting(
    SALES_EMAIL_KEYS.brochureMaxUploadMb,
    update.brochureMaxUploadMb,
    portId,
    meta,
  );
  await writeSetting(
    SALES_EMAIL_KEYS.emailAttachThresholdMb,
    update.emailAttachThresholdMb,
    portId,
    meta,
  );

  // Password fields: encrypt before write. Empty string = "no change".
  if (update.smtpPass !== undefined && update.smtpPass !== '') {
    if (update.smtpPass === null) {
      await upsertSetting(SALES_EMAIL_KEYS.smtpPassEncrypted, null, portId, meta);
    } else {
      await upsertSetting(
        SALES_EMAIL_KEYS.smtpPassEncrypted,
        encrypt(update.smtpPass),
        portId,
        meta,
      );
    }
  }
  if (update.imapPass !== undefined && update.imapPass !== '') {
    if (update.imapPass === null) {
      await upsertSetting(SALES_EMAIL_KEYS.imapPassEncrypted, null, portId, meta);
    } else {
      await upsertSetting(
        SALES_EMAIL_KEYS.imapPassEncrypted,
        encrypt(update.imapPass),
        portId,
        meta,
      );
    }
  }
}

// ─── Transporter factory ─────────────────────────────────────────────────────

export type SenderAccount = 'noreply' | 'sales';

/**
 * Build a nodemailer transporter for the requested sender account.
 *
 * - `'sales'` uses the per-port SALES_* keys (see {@link getSalesEmailConfig}).
 * - `'noreply'` falls back to the legacy `port-config.ts` resolver (see
 *   `getPortEmailConfig`) which itself drops back to the env defaults.
 *
 * Throws when the requested account isn't configured well enough to send;
 * callers should let this propagate so the document_sends row gets a
 * `failedAt` + `errorReason`.
 */
export async function createSalesTransporter(portId: string): Promise<{
  transporter: Transporter;
  fromAddress: string;
  authedUser: string | null;
}> {
  const cfg = await getSalesEmailConfig(portId);
  if (!cfg.smtpHost) {
    throw new ConflictError(
      'Sales SMTP not configured for this port. Configure in /admin/email before sending.',
    );
  }
  const transporter = nodemailer.createTransport({
    host: cfg.smtpHost,
    port: cfg.smtpPort,
    secure: cfg.smtpSecure,
    // H-08: cap connection / greeting / socket timeouts so a hung SMTP
    // relay can't stall the BullMQ email queue indefinitely (default
    // would let a single flaky upstream block every worker slot for
    // ~10 min before the retry policy gives up).
    ...SMTP_TIMEOUTS,
    ...(cfg.smtpUser && cfg.smtpPass ? { auth: { user: cfg.smtpUser, pass: cfg.smtpPass } } : {}),
  });
  return { transporter, fromAddress: cfg.fromAddress, authedUser: cfg.smtpUser };
}

/**
 * Public-facing sanitizer - strips encrypted fields and replaces password
 * fields with a boolean `isSet` marker. Used by the admin GET endpoint so
 * reps with `manage_settings` can see whether creds are configured without
 * the API ever returning the ciphertext (much less plaintext).
 */
export function redactSalesConfigForResponse(
  cfg: SalesEmailConfig,
  imap: SalesImapConfig,
  content: SalesContentConfig,
): {
  email: Omit<SalesEmailConfig, 'smtpPass'> & { smtpPassIsSet: boolean };
  imap: Omit<SalesImapConfig, 'imapPass'> & { imapPassIsSet: boolean };
  content: SalesContentConfig;
} {
  // Spread without the password fields - never reflect the decrypted value
  // (or its ciphertext) back to the API surface.
  const email = {
    fromAddress: cfg.fromAddress,
    smtpHost: cfg.smtpHost,
    smtpPort: cfg.smtpPort,
    smtpSecure: cfg.smtpSecure,
    smtpUser: cfg.smtpUser,
    authMethod: cfg.authMethod,
    isUsable: cfg.isUsable,
    smtpPassIsSet: Boolean(cfg.smtpPass),
  };
  const imapRedacted = {
    imapHost: imap.imapHost,
    imapPort: imap.imapPort,
    imapUser: imap.imapUser,
    isUsable: imap.isUsable,
    imapPassIsSet: Boolean(imap.imapPass),
  };
  return { email, imap: imapRedacted, content };
}
