import { createHmac, timingSafeEqual } from 'node:crypto';

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

import { env } from '../env.js';
import { audit } from '../services/audit.js';
import { isSuppressed } from '../services/suppressions.js';
import { log } from './logger.js';

/**
 * Transactional email client — talks to mittera.eu's REST API at
 * /api/v1/emails using Bearer-token auth. Inbound delivery / bounce /
 * complaint events arrive at /mittera-webhook signed with mittera's
 * X-mittera-Signature scheme; verifySignature below mirrors mittera's
 * own SDK so the receiver stays in lock-step with the publisher.
 *
 * Outbound request shape:
 *   POST {BRIVEN_MITTERA_API_URL}/api/v1/emails
 *   Authorization: Bearer {BRIVEN_MITTERA_API_KEY}
 *   Content-Type: application/json
 *   { from, to, subject, html, text }
 *
 * Inbound webhook headers (verified by verifySignature, not produced
 * by this module):
 *   X-mittera-Signature: v1=<hex_hmac_sha256("${ts_ms}.${rawBody}")>
 *   X-mittera-Timestamp: <unix_milliseconds>
 *
 * In dev (no API key configured) emails print to stdout so the
 * first-user bootstrap flow still works on a fresh self-host.
 */

const SEND_PATH = '/api/v1/emails';
const SIGNATURE_PREFIX = 'v1=';
const DEFAULT_TOLERANCE_MS = 5 * 60 * 1000;

interface SendArgs {
  to: string;
  subject: string;
  html: string;
  text: string;
  /**
   * Optional From: override. When set, replaces the global `fromAddress()`
   * default — used by briven auth for per-tenant senders (mittera-verified
   * customer domain) per BUILD_PLAN.md §8.
   */
  from?: string;
  /**
   * Optional tenant context. Threaded into the audit log row so admin
   * email-events streams can scope by project. Default null preserves
   * existing control-plane behavior.
   */
  projectId?: string | null;
}

function fromAddress(): string {
  // Use the configured public domain so the From: matches the deployment.
  // Falls back to a literal in dev so the env never has to be set locally.
  const domain = env.BRIVEN_DOMAIN ?? 'briven.local';
  return `briven <noreply@${domain}>`;
}

function isConfigured(): boolean {
  return Boolean(env.BRIVEN_MITTERA_API_URL && env.BRIVEN_MITTERA_API_KEY);
}

/**
 * SMTP is the real delivery path. It's "configured" only when HOST + USER
 * + PASS are all non-empty (loadEnv treats empty strings as unset, so a
 * blank var here reads as undefined). When configured, SMTP becomes the
 * PRIMARY sender ahead of mittera — mittera accepts sends but never
 * delivers, so a wired SMTP provider (Resend / Mailgun / Postmark / SES)
 * takes precedence.
 */
function isSmtpConfigured(): boolean {
  return Boolean(env.BRIVEN_SMTP_HOST && env.BRIVEN_SMTP_USER && env.BRIVEN_SMTP_PASS);
}

/**
 * Lazily-built, module-scoped nodemailer transporter singleton. Built once
 * on first send and reused for the pooled connection — rebuilding per send
 * would defeat nodemailer's connection reuse. `secure` is true only on 465
 * (implicit TLS); 587 uses STARTTLS which nodemailer negotiates itself. The
 * ~10s greeting/socket timeouts mirror the mittera path's 10s AbortSignal
 * so a hung SMTP host can't tie up the magic-link request.
 */
let smtpTransporter: Transporter | null = null;

function getSmtpTransporter(): Transporter {
  if (!smtpTransporter) {
    const port = env.BRIVEN_SMTP_PORT;
    smtpTransporter = nodemailer.createTransport({
      host: env.BRIVEN_SMTP_HOST!,
      port,
      secure: port === 465,
      auth: {
        user: env.BRIVEN_SMTP_USER!,
        pass: env.BRIVEN_SMTP_PASS!,
      },
      // Hard caps so a hung SMTP server doesn't tie up the request that
      // triggered the send (mirrors the mittera fetch's 10s AbortSignal).
      greetingTimeout: 10_000,
      socketTimeout: 10_000,
      connectionTimeout: 10_000,
    });
  }
  return smtpTransporter;
}

/**
 * The From: SMTP sends use.
 * Prefer per-call override first (project Auth branding:
 * `Pando <noreply@pando.so>`) — SuperTokens-style multi-app senders.
 * Then platform BRIVEN_SMTP_FROM, then global default.
 *
 * Note: the mail provider must allow the domain (SPF/DKIM). Unverified
 * custom domains may bounce; operators set domain in Auth → branding.
 */
function smtpFrom(args: SendArgs): string {
  return args.from ?? env.BRIVEN_SMTP_FROM ?? fromAddress();
}

/**
 * Send via the real SMTP provider. On success logs `smtp_send_ok` and
 * writes an audit row with action `smtp.<label>.sent` carrying the
 * provider messageId — mirroring the mittera path's `mittera.<label>.sent`
 * so the Email Admin cockpit's per-template stats pick it up automatically
 * (SEND_ACTION_RE already matches `smtp\.(.+)\.sent`). On failure logs
 * `smtp_send_failed` and throws so the caller sees the error.
 */
async function sendViaSmtp(label: string, args: SendArgs): Promise<void> {
  let messageId: string | null = null;
  try {
    const info = await getSmtpTransporter().sendMail({
      from: smtpFrom(args),
      to: args.to,
      subject: args.subject,
      html: args.html,
      text: args.text,
    });
    messageId = info.messageId ?? null;
  } catch (err) {
    log.error('smtp_send_failed', {
      label,
      error: err instanceof Error ? err.message : String(err),
    });
    throw err instanceof Error ? err : new Error(`smtp send failed: ${String(err)}`);
  }

  log.info('smtp_send_ok', { label, messageId });

  // Audit-log the send so operators see it in the admin email-events
  // stream, tagged with the template. Same audit() call shape as the
  // mittera path so aggregateTemplateStats attributes it identically;
  // recipient redacted per CLAUDE.md §5.1.
  await audit({
    actorId: null,
    projectId: args.projectId ?? null,
    action: `smtp.${label}.sent`,
    ipHash: null,
    userAgent: 'briven-api',
    metadata: {
      messageId,
      recipientRedacted: redactEmail(args.to),
      subject: args.subject,
    },
  });
}

/**
 * What the admin cockpit can TRUTHFULLY report about the live sender,
 * read straight off this module's own config + transport helpers so the
 * dashboard never drifts from the real send path (Phase 8 §1).
 *
 * Note on provider: Briven talks only to mittera.eu, which abstracts the
 * underlying provider (SES / Mailgun / Pando). That provider is NOT
 * reported back to Briven on the send path or the webhook envelope, so we
 * deliberately do not invent a provider field here — `activeTransport`
 * reports the leg Briven itself drives (smtp vs mittera vs the dev stdout
 * sink), which is the part Briven can actually observe. When a real SMTP
 * provider is configured (HOST + USER + PASS) it is the PRIMARY sender —
 * `smtpFallbackConfigured` is true and `activeTransport` is 'smtp';
 * otherwise the send path falls back to mittera, then dev stdout.
 */
export interface EmailSenderInfo {
  /** The default From: every control-plane send uses (per-tenant sends override it). */
  fromAddress: string;
  mitteraConfigured: boolean;
  /** The exact mittera POST endpoint, or null when unconfigured. */
  mitteraEndpoint: string | null;
  smtpFallbackConfigured: boolean;
  /** The transport that WOULD carry the next control-plane send given current config. */
  activeTransport: 'mittera' | 'smtp' | 'dev-stdout';
}

export function getEmailSenderInfo(): EmailSenderInfo {
  const smtp = isSmtpConfigured();
  const mittera = isConfigured();
  return {
    fromAddress: fromAddress(),
    mitteraConfigured: mittera,
    mitteraEndpoint: env.BRIVEN_MITTERA_API_URL
      ? `${env.BRIVEN_MITTERA_API_URL.replace(/\/$/, '')}${SEND_PATH}`
      : null,
    smtpFallbackConfigured: smtp,
    // Same precedence as send(): SMTP is primary when configured, then
    // mittera, then the dev stdout sink.
    activeTransport: smtp ? 'smtp' : mittera ? 'mittera' : 'dev-stdout',
  };
}

async function send(label: string, args: SendArgs): Promise<void> {
  // Suppression guard — never POST to mittera for a recipient on the
  // local suppression list (permanent bounce, complaint, mittera-side
  // suppression). Cheaper than a 4xx + retry storm; protects sender
  // reputation from re-sending to a known-bad address.
  if (await isSuppressed(args.to)) {
    log.warn(`${label}_recipient_suppressed`, {
      // recipient logged ONLY at this stage — already on our suppression
      // list, not new PII.
      to: args.to,
    });
    return;
  }

  // Primary path: real SMTP (Resend / Mailgun / SES via SMTP).
  // If SMTP is misconfigured (bad key), fall through to mittera so Auth OTP
  // does not hard-fail (flndrn 2026-07-29: 535 invalid login broke krypco OTP).
  if (isSmtpConfigured()) {
    try {
      await sendViaSmtp(label, args);
      return;
    } catch (err) {
      log.warn('smtp_send_failed_fallback_mittera', {
        label,
        error: err instanceof Error ? err.message : String(err),
      });
      // continue to mittera / dev below
    }
  }

  // Fallback: mittera's REST API (kept as the fallback, not deleted).
  // Dev fallback: print so j can complete bootstrap without external email.
  if (!isConfigured()) {
    log.warn(`${label}_logged_only`);
    process.stdout.write(`\n  ${label} (dev only):\n  to: ${args.to}\n  subject: ${args.subject}\n\n`);
    return;
  }

  const body = JSON.stringify({
    from: args.from ?? fromAddress(),
    to: args.to,
    subject: args.subject,
    html: args.html,
    text: args.text,
  });

  const url = `${env.BRIVEN_MITTERA_API_URL!.replace(/\/$/, '')}${SEND_PATH}`;

  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      authorization: `Bearer ${env.BRIVEN_MITTERA_API_KEY!}`,
    },
    body,
    // Hard cap so a hung mittera doesn't tie up the magic-link request.
    signal: AbortSignal.timeout(10_000),
  });

  if (!res.ok) {
    const text = await res.text().catch(() => '');
    log.error('mittera_send_failed', {
      status: res.status,
      label,
      // Truncate so a misconfigured server returning HTML doesn't bloat logs.
      body: text.slice(0, 240),
    });
    throw new Error(`mittera send failed: ${res.status}`);
  }

  // Successful POST. mittera returns the email id under `emailId`
  // (verified empirically; `id`/`messageId` are accepted as fallbacks
  // in case the API shape evolves). The id is what shows up later in
  // delivery / bounce webhook events under `messageId`, so capturing
  // it here lets an operator correlate "did my magic link ship?" with
  // "did mittera accept it / did the recipient bounce?" via grep.
  const responseBody = await res.text().catch(() => '');
  let messageId: string | null = null;
  try {
    const parsed = JSON.parse(responseBody) as {
      emailId?: string;
      id?: string;
      messageId?: string;
    };
    messageId = parsed.emailId ?? parsed.id ?? parsed.messageId ?? null;
  } catch {
    // Non-JSON body — log raw so we can see what mittera actually returned.
  }
  log.info('mittera_send_ok', {
    label,
    status: res.status,
    messageId,
    bodyPreview: messageId ? undefined : responseBody.slice(0, 240),
  });

  // Audit-log the send so operators can see it in the admin email-events
  // stream alongside inbound webhook events. Recipient is redacted per
  // CLAUDE.md §5.1; the messageId is the authoritative correlation key
  // (mittera echoes it back on delivery / bounce / complaint webhooks).
  await audit({
    actorId: null,
    projectId: args.projectId ?? null,
    action: `mittera.${label}.sent`,
    ipHash: null,
    userAgent: 'briven-api',
    metadata: {
      messageId,
      recipientRedacted: redactEmail(args.to),
      subject: args.subject,
    },
  });
}

/**
 * Per-tenant send entry point — used by briven auth's customer-facing
 * email flows (BUILD_PLAN.md §8). Same mittera POST + suppression +
 * audit chain as the control-plane sends; only the From: address and
 * audit projectId are tenant-scoped.
 *
 * Caller resolves the From: via `getAuthConfig(projectId).branding`:
 *   - verified `senderDomain` → `senderName <noreply@<senderDomain>>`
 *   - unset / unverified domain → fallback `briven auth <noreply@auth.briven.tech>`
 */
export async function sendTenantEmail(
  label: string,
  args: SendArgs & { from: string; projectId: string },
): Promise<void> {
  await send(label, args);
}

/**
 * Generic transactional send for briven-engine Auth (OTP, magic link, etc.).
 * Same chain as platform mail: SMTP primary → mittera → dev stdout.
 */
export async function sendTransactional(
  label: string,
  args: {
    to: string;
    subject: string;
    html: string;
    text: string;
    projectId?: string | null;
    from?: string;
  },
): Promise<void> {
  await send(label, {
    to: args.to,
    subject: args.subject,
    html: args.html,
    text: args.text,
    projectId: args.projectId ?? null,
    from: args.from,
  });
}

/**
 * `flandriendev@hotmail.com` → `f•••v@h•••m`. Enough for an operator to
 * disambiguate two recent sends in the admin stream without surfacing
 * the full address. Same pattern documented in CLAUDE.md §5.1.
 * Exported for tests.
 */
export function redactEmail(email: string): string {
  const at = email.indexOf('@');
  if (at < 0) return '•••';
  const local = email.slice(0, at);
  const domain = email.slice(at + 1);
  const head = (s: string): string => {
    if (s.length === 0) return '';
    if (s.length === 1) return s;
    return `${s[0]}•••${s[s.length - 1]}`;
  };
  return `${head(local)}@${head(domain)}`;
}

export async function sendMagicLink(to: string, url: string): Promise<void> {
  await send('magic_link', {
    to,
    subject: 'your briven sign-in link',
    html: magicLinkHtml(url),
    text: magicLinkText(url),
  });
}

export async function sendInvitation(to: string, url: string): Promise<void> {
  await send('invitation', {
    to,
    subject: 'you were invited to a briven project',
    html: invitationHtml(url),
    text: invitationText(url),
  });
}

export async function sendEmailVerification(to: string, url: string): Promise<void> {
  await send('verify_email', {
    to,
    subject: 'verify your briven email',
    html: verifyEmailHtml(url),
    text: verifyEmailText(url),
  });
}

export async function sendEmailChangeConfirmation(
  to: string,
  newEmail: string,
  url: string,
): Promise<void> {
  await send('email_change_confirmation', {
    to,
    subject: 'confirm your new briven sign-in email',
    html: emailChangeHtml(newEmail, url),
    text: emailChangeText(newEmail, url),
  });
}

export async function sendPasswordReset(to: string, url: string): Promise<void> {
  await send('reset_password', {
    to,
    subject: 'reset your briven password',
    html: resetPasswordHtml(url),
    text: resetPasswordText(url),
  });
}

/**
 * Confirmation that an account-deletion request was received. Sent
 * *before* the cascade runs so the user has a paper trail even if the
 * mailbox attached to the account is the one they're closing. Includes
 * the 30-day reversal window — operator support can revert within that
 * window before the hard-delete cron runs.
 */
export async function sendAccountDeletionConfirmation(to: string): Promise<void> {
  await send('account_deletion', {
    to,
    subject: 'your briven account is being deleted',
    html: accountDeletionHtml(),
    text: accountDeletionText(),
  });
}

export interface MigrationRequestEmailInput {
  requestId: string;
  source: string;
  contactEmail: string;
  sourceUrl: string | null;
  urgency: string;
  estimatedTables: number | null;
  estimatedRows: string | null;
  estimatedFunctions: number | null;
  sourceNotes: string;
}

/**
 * Confirms to the customer that their migration intake was received,
 * shows the briven request id (to quote if they email migrations@), and
 * sets the expectation for next contact (one business day).
 */
export async function sendMigrationRequestCustomerConfirmation(
  input: MigrationRequestEmailInput,
): Promise<void> {
  await send('migration_request_confirmation', {
    to: input.contactEmail,
    subject: `we got your migration request · ${input.requestId}`,
    html: migrationCustomerHtml(input),
    text: migrationCustomerText(input),
  });
}

export interface MigrationStatusUpdateInput {
  requestId: string;
  source: string;
  contactEmail: string;
  oldStatus: string;
  newStatus: string;
  operatorMessage?: string;
}

/**
 * Auto-fires whenever an operator flips a migration request's status.
 * Skipped for the transition that happens at creation (already covered
 * by the customer confirmation email). Skipped for the `new → contacted`
 * transition because the operator is about to email manually anyway —
 * a status-change email on top would arrive twice. Skipped for
 * operator-notes-only edits.
 */
export async function sendMigrationStatusUpdate(
  input: MigrationStatusUpdateInput,
): Promise<void> {
  await send('migration_status_update', {
    to: input.contactEmail,
    subject: `your migration · ${migrationStatusHeadline(input.newStatus, input.source)}`,
    html: migrationStatusUpdateHtml(input),
    text: migrationStatusUpdateText(input),
  });
}

function migrationStatusHeadline(status: string, source: string): string {
  switch (status) {
    case 'scheduled':
      return `${source} migration scheduled`;
    case 'in_progress':
      return `${source} migration in progress`;
    case 'completed':
      return `${source} migration completed`;
    case 'cancelled':
      return `${source} migration cancelled`;
    case 'contacted':
      return `we’ve reached out about your ${source} migration`;
    default:
      return `${source} migration updated`;
  }
}

/**
 * Notifies the operator inbox (default: migrations@<domain>) that a new
 * intake landed. Includes everything the customer submitted so the
 * operator can triage from the inbox without opening the dashboard for
 * the first read.
 */
export async function sendMigrationRequestOperatorAlert(
  input: MigrationRequestEmailInput,
): Promise<void> {
  const inbox = env.BRIVEN_MIGRATIONS_INBOX;
  await send('migration_request_alert', {
    to: inbox,
    subject: `new migration request · ${input.source} · ${input.urgency.replace(/_/g, ' ')}`,
    html: migrationOperatorHtml(input),
    text: migrationOperatorText(input),
  });
}

/**
 * Verify a `X-mittera-Signature: v1=<hex>` header against the raw
 * request body using the shared webhook secret. The timestamp lives
 * in a separate `X-mittera-Timestamp` header (unix milliseconds) and
 * is checked against `nowMs` (default `Date.now()`) with the configured
 * `toleranceMs` (default ±5 min) to defeat replay.
 *
 * Mirrors `@mittera/sdk`'s Webhooks verifier exactly so a future swap
 * to the SDK is one-line.
 */
export function verifySignature(args: {
  secret: string;
  signatureHeader: string | null;
  timestampHeader: string | null;
  body: string;
  toleranceMs?: number;
  nowMs?: number;
}): boolean {
  if (!args.signatureHeader || !args.timestampHeader) return false;
  if (!args.signatureHeader.startsWith(SIGNATURE_PREFIX)) return false;

  const ts = Number(args.timestampHeader);
  if (!Number.isFinite(ts)) return false;

  const tolerance = args.toleranceMs ?? DEFAULT_TOLERANCE_MS;
  const now = args.nowMs ?? Date.now();
  if (Math.abs(now - ts) > tolerance) return false;

  const expected = `${SIGNATURE_PREFIX}${createHmac('sha256', args.secret)
    .update(`${args.timestampHeader}.${args.body}`)
    .digest('hex')}`;

  if (expected.length !== args.signatureHeader.length) return false;
  try {
    return timingSafeEqual(
      Buffer.from(expected, 'utf8'),
      Buffer.from(args.signatureHeader, 'utf8'),
    );
  } catch {
    return false;
  }
}

/* -------------------------------------------------------------------------- */
/* Email HTML — dark palette, single column, primary CTA in brand green.      */
/* -------------------------------------------------------------------------- */

function magicLinkHtml(url: string): string {
  return shell(
    'sign in to briven',
    `
    <p>click the button below to sign in. this link expires in 10 minutes.</p>
    ${cta('sign in', url)}
    <p class="muted">if you didn't request this, you can ignore this email.</p>
  `,
  );
}

function magicLinkText(url: string): string {
  return `sign in to briven\n\n${url}\n\nthis link expires in 10 minutes. if you didn't request it, ignore this email.`;
}

function invitationHtml(url: string): string {
  return shell(
    'you were invited to a briven project',
    `
    <p>accept the invitation to join the project on briven. the link expires in 7 days.</p>
    ${cta('accept invitation', url)}
    <p class="muted">if you weren't expecting this, ignore the email — nothing happens.</p>
  `,
  );
}

function invitationText(url: string): string {
  return `you were invited to a briven project\n\n${url}\n\nexpires in 7 days.`;
}

function verifyEmailHtml(url: string): string {
  return shell(
    'verify your briven email',
    `
    <p>confirm this address so we can reach you about your briven account.</p>
    ${cta('verify email', url)}
  `,
  );
}

function verifyEmailText(url: string): string {
  return `verify your briven email\n\n${url}\n`;
}

function resetPasswordHtml(url: string): string {
  return shell(
    'reset your briven password',
    `
    <p>click below to set a new password. this link expires in 1 hour.</p>
    ${cta('reset password', url)}
    <p class="muted">if you didn't request a reset, you can ignore this email — your password stays unchanged.</p>
  `,
  );
}

function resetPasswordText(url: string): string {
  return `reset your briven password\n\n${url}\n\nthis link expires in 1 hour. if you didn't request a reset, ignore this email.`;
}

function emailChangeHtml(newEmail: string, url: string): string {
  return shell(
    'confirm your new briven sign-in email',
    `
    <p>we received a request to change the sign-in email on your briven account to <strong>${escapeHtml(newEmail)}</strong>.</p>
    <p>click the button below to confirm. this link expires in 1 hour.</p>
    ${cta('confirm new email', url)}
    <p class="muted">if you didn't request this, ignore this email — your current sign-in email stays unchanged. if you keep getting these, email support@flndrn.com.</p>
  `,
  );
}

function emailChangeText(newEmail: string, url: string): string {
  return `confirm your new briven sign-in email\n\nwe received a request to change your sign-in email to ${newEmail}.\n\n${url}\n\nthis link expires in 1 hour. if you didn't request this, ignore this email.`;
}

function accountDeletionHtml(): string {
  return shell(
    'your briven account is being deleted',
    `
    <p>we received your account deletion request and started the process.</p>
    <ul style="color:#9ba3af;font-size:15px;padding-left:18px">
      <li>your personal data has been cleared from our control plane (legal name, address, vat id, display name, profile picture).</li>
      <li>projects owned only by you have been soft-deleted and stop accepting traffic immediately.</li>
      <li>team orgs where you're not the only owner stay live; you've been removed from membership.</li>
      <li>api keys you owned are revoked.</li>
    </ul>
    <p>if you have a paid subscription, manage cancellation on polar via your billing portal — we don't auto-cancel.</p>
    <p>you have <strong>30 days</strong> to change your mind: email support@flndrn.com from this address and we can revert. after that the soft-delete becomes a hard-delete and we can't get the data back.</p>
    <p class="muted">if you did not request this, contact support@flndrn.com immediately.</p>
  `,
  );
}

function accountDeletionText(): string {
  return [
    'your briven account is being deleted',
    '',
    'we received your account deletion request and started the process.',
    '',
    '- personal data cleared from our control plane.',
    '- projects owned only by you soft-deleted, traffic stopped.',
    '- team orgs where you are not the sole owner stay live.',
    '- api keys revoked.',
    '',
    'if you have a paid subscription, cancel via polar billing portal — we do not auto-cancel.',
    '',
    `you have 30 days to revert: email support@flndrn.com from this address. after that the delete is permanent.`,
    '',
    `if you did not request this, contact support@flndrn.com immediately.`,
  ].join('\n');
}

function escapeHtml(s: string): string {
  return s
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

// Customer-visible contact addresses route to the parent flndrn Limited
// inbox (admin.flndrn.com queue). Outbound product From: stays on
// briven.tech for SPF/DKIM alignment — only inbound contact moves.
const MIGRATIONS_CONTACT = 'migrations@flndrn.com';

function migrationCustomerHtml(input: MigrationRequestEmailInput): string {
  return shell(
    `we got your migration request from ${escapeHtml(input.source)}`,
    `
    <p>thanks for asking us to help move your project to briven.</p>
    <p>an operator will reach out from <code>${MIGRATIONS_CONTACT}</code> within one business day with the next steps — typically a short call to confirm scope, then the actual data + functions move while you keep running on your current platform.</p>
    <p style="background:#1a1d24;border-radius:8px;padding:12px;font-family:ui-monospace,SFMono-Regular,monospace;font-size:13px;color:#9ba3af;border:1px solid #2a2e36">
      request id: ${escapeHtml(input.requestId)}<br/>
      source: ${escapeHtml(input.source)}<br/>
      urgency: ${escapeHtml(input.urgency.replace(/_/g, ' '))}
    </p>
    <p class="muted">your ${escapeHtml(input.source)} stays untouched. we only read from it. nothing on your source is moved or modified until you press the cutover button — which we won't do until you say so.</p>
    <p class="muted">questions or follow-ups: reply to this email, or write to ${MIGRATIONS_CONTACT} and quote the request id above.</p>
  `,
  );
}

function migrationCustomerText(input: MigrationRequestEmailInput): string {
  return [
    `we got your migration request from ${input.source}`,
    '',
    'thanks for asking us to help move your project to briven. an operator will reach out within one business day with the next steps.',
    '',
    `request id: ${input.requestId}`,
    `source: ${input.source}`,
    `urgency: ${input.urgency.replace(/_/g, ' ')}`,
    '',
    `your ${input.source} stays untouched. we only read from it. nothing on your source is moved or modified until you press the cutover button.`,
    '',
    `questions or follow-ups: reply to this email, or write to ${MIGRATIONS_CONTACT} and quote the request id.`,
  ].join('\n');
}

function migrationOperatorHtml(input: MigrationRequestEmailInput): string {
  const domain = env.BRIVEN_DOMAIN ?? 'briven.tech';
  return shell(
    `new migration request · ${escapeHtml(input.source)}`,
    `
    <p><strong>${escapeHtml(input.contactEmail)}</strong> requested a migration from <strong>${escapeHtml(input.source)}</strong>.</p>
    <p style="background:#1a1d24;border-radius:8px;padding:12px;font-family:ui-monospace,SFMono-Regular,monospace;font-size:13px;color:#9ba3af;border:1px solid #2a2e36">
      request id: ${escapeHtml(input.requestId)}<br/>
      contact: ${escapeHtml(input.contactEmail)}<br/>
      source: ${escapeHtml(input.source)}<br/>
      urgency: ${escapeHtml(input.urgency.replace(/_/g, ' '))}<br/>
      source URL: ${input.sourceUrl ? escapeHtml(input.sourceUrl) : '—'}<br/>
      tables: ${input.estimatedTables ?? '—'} · rows: ${escapeHtml(input.estimatedRows ?? '—')} · functions: ${input.estimatedFunctions ?? '—'}
    </p>
    ${
      input.sourceNotes
        ? `<p style="font-family:ui-monospace,SFMono-Regular,monospace;font-size:13px;white-space:pre-wrap;background:#1a1d24;border-radius:8px;padding:12px;color:#d1d5db;border:1px solid #2a2e36">${escapeHtml(input.sourceNotes)}</p>`
        : '<p class="muted">no extra notes from the customer.</p>'
    }
    ${cta('open in admin', `https://${domain}/dashboard/admin/migrations`)}
  `,
  );
}

function statusBlurb(status: string): string {
  switch (status) {
    case 'contacted':
      return 'we’ve reached out — check your inbox for a reply with next steps.';
    case 'scheduled':
      return 'a migration window has been scheduled. you should have a calendar invite or proposed time from us.';
    case 'in_progress':
      return 'we’re moving your project right now. you’ll get another update when we’re done. your current platform stays untouched until the cutover step you control.';
    case 'completed':
      return 'your migration is complete on the briven side. open the dashboard to verify your data and run the cutover when you’re ready.';
    case 'cancelled':
      return 'we’ve cancelled this request. nothing changed on your current platform.';
    default:
      return 'status updated.';
  }
}

function migrationStatusUpdateHtml(input: MigrationStatusUpdateInput): string {
  const domain = env.BRIVEN_DOMAIN ?? 'briven.tech';
  const dashboardHref = `https://${domain}/dashboard/migrations`;
  return shell(
    migrationStatusHeadline(input.newStatus, input.source),
    `
    <p>${statusBlurb(input.newStatus)}</p>
    ${
      input.operatorMessage
        ? `<p style="background:#1a1d24;border-radius:8px;padding:12px;white-space:pre-wrap;color:#d1d5db;border:1px solid #2a2e36;font-size:14px">${escapeHtml(input.operatorMessage)}</p>`
        : ''
    }
    <p style="background:#1a1d24;border-radius:8px;padding:12px;font-family:ui-monospace,SFMono-Regular,monospace;font-size:13px;color:#9ba3af;border:1px solid #2a2e36">
      request id: ${escapeHtml(input.requestId)}<br/>
      source: ${escapeHtml(input.source)}<br/>
      status: ${escapeHtml(input.oldStatus.replace(/_/g, ' '))} → <strong style="color:#f5f7fa">${escapeHtml(input.newStatus.replace(/_/g, ' '))}</strong>
    </p>
    ${cta('open dashboard', dashboardHref)}
    <p class="muted">need a human? reply to this email or write to ${MIGRATIONS_CONTACT} and quote the request id.</p>
  `,
  );
}

function migrationStatusUpdateText(input: MigrationStatusUpdateInput): string {
  const domain = env.BRIVEN_DOMAIN ?? 'briven.tech';
  return [
    migrationStatusHeadline(input.newStatus, input.source),
    '',
    statusBlurb(input.newStatus),
    '',
    ...(input.operatorMessage ? [input.operatorMessage, ''] : []),
    `request id: ${input.requestId}`,
    `source: ${input.source}`,
    `status: ${input.oldStatus.replace(/_/g, ' ')} → ${input.newStatus.replace(/_/g, ' ')}`,
    '',
    `open dashboard: https://${domain}/dashboard/migrations`,
    '',
    `need a human? reply to this email or write to ${MIGRATIONS_CONTACT}.`,
  ].join('\n');
}

function migrationOperatorText(input: MigrationRequestEmailInput): string {
  const domain = env.BRIVEN_DOMAIN ?? 'briven.tech';
  return [
    `new migration request · ${input.source}`,
    '',
    `${input.contactEmail} requested a migration from ${input.source}.`,
    '',
    `request id: ${input.requestId}`,
    `contact: ${input.contactEmail}`,
    `source: ${input.source}`,
    `urgency: ${input.urgency.replace(/_/g, ' ')}`,
    `source URL: ${input.sourceUrl ?? '—'}`,
    `tables: ${input.estimatedTables ?? '—'} · rows: ${input.estimatedRows ?? '—'} · functions: ${input.estimatedFunctions ?? '—'}`,
    '',
    'customer notes:',
    input.sourceNotes || '(none)',
    '',
    `open in admin: https://${domain}/dashboard/admin/migrations`,
  ].join('\n');
}

function cta(label: string, href: string): string {
  return `<p style="margin:32px 0"><a href="${href}" style="display:inline-block;background:#00e87a;color:#0a0b0d;padding:12px 24px;border-radius:10px;font-weight:500;font-family:system-ui,sans-serif;text-decoration:none">${label}</a></p>`;
}

function shell(title: string, body: string): string {
  const domain = env.BRIVEN_DOMAIN ?? 'briven.tech';
  return `<!doctype html>
<html><head><meta charset="utf-8"><meta name="color-scheme" content="dark"><title>${title}</title></head>
<body style="margin:0;background:#0a0b0d;color:#f5f7fa;font-family:system-ui,-apple-system,sans-serif;line-height:1.6">
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#0a0b0d">
    <tr><td align="center" style="padding:32px 16px">
      <table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;width:100%;background:#13151a;border:1px solid #2a2e36;border-radius:14px;padding:32px">
        <tr><td>
          <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 16px 0">
            <tr>
              <td style="padding-right:10px;vertical-align:middle"><img src="https://${domain}/icon-email.png" alt="" width="32" height="32" style="display:block;border:0;outline:none" /></td>
              <td style="vertical-align:middle"><span style="font-family:system-ui,sans-serif;font-size:20px;font-weight:500;letter-spacing:-0.02em;color:#f5f7fa">briven</span></td>
            </tr>
          </table>
          <h2 style="font-family:system-ui,sans-serif;font-size:18px;font-weight:500;margin:0 0 16px 0">${title}</h2>
          <div style="color:#9ba3af;font-size:15px">${body}</div>
          <p style="color:#6b7280;font-size:12px;margin-top:32px;border-top:1px solid #1e2128;padding-top:16px">
            briven · <a style="color:#9ba3af" href="https://${domain}">${domain}</a><br/>
            made with <span style="color:#e8344a">&#9829;</span> in Flanders by flndrn<br/>
            100% self-funded, sustainable &amp; independent<br/>
            flndrn Limited, Limassol, Cyprus
          </p>
        </td></tr>
      </table>
    </td></tr>
  </table>
  <style>.muted { color:#6b7280;font-size:13px }</style>
</body></html>`;
}

/* ─── RESTORED AFTER MERGE LOSS (2026-07-02): support-ticket emails ── */
// This block was dropped when lib/email.ts was rewritten during a branch
// merge; routes/contact.ts and services/support-tickets.ts still import
// these senders. Restored verbatim from d48bceb.

/* ─── support tickets ────────────────────────────────────────────── */

/**
 * Confirms to the sender that their tagged /contact submission became a
 * support ticket, and gives them the ticket number to quote. Sent on
 * ticket creation. `ticketNumber` is the rendered, '#'-prefixed value.
 */
export async function sendTicketCreatedConfirmation(
  to: string,
  ticketNumber: string,
): Promise<void> {
  await send('ticket_created', {
    to,
    subject: `we got your support request · ${ticketNumber}`,
    html: ticketCreatedHtml(ticketNumber),
    text: ticketCreatedText(ticketNumber),
  });
}

/**
 * Emails the sender an operator's reply on their ticket. `body` is the
 * operator's message text; `ticketNumber` is the rendered '#'-prefixed value.
 */
export async function sendTicketReply(
  to: string,
  ticketNumber: string,
  body: string,
): Promise<void> {
  await send('ticket_reply', {
    to,
    subject: `re: your support request · ${ticketNumber}`,
    html: ticketReplyHtml(ticketNumber, body),
    text: ticketReplyText(ticketNumber, body),
  });
}

/**
 * Emails the sender an operator's reply to a PLAIN (untagged) contact
 * message — one that never became a support ticket, so there's no ticket
 * number to quote. Same transport chain as sendTicketReply; the copy just
 * references "your message to briven" instead of a ticket. `body` is the
 * operator's message text.
 */
export async function sendContactReply(to: string, body: string): Promise<void> {
  await send('contact_reply', {
    to,
    subject: 're: your message to briven',
    html: contactReplyHtml(body),
    text: contactReplyText(body),
  });
}

/**
 * Notifies the sender that the status of their ticket changed (used for
 * the 'replied'/'closed' transitions). `status` is the new status code.
 */
export async function sendTicketStatusUpdate(
  to: string,
  ticketNumber: string,
  status: string,
): Promise<void> {
  await send('ticket_status_update', {
    to,
    subject: `update on your support request · ${ticketNumber}`,
    html: ticketStatusHtml(ticketNumber, status),
    text: ticketStatusText(ticketNumber, status),
  });
}

const SUPPORT_CONTACT = 'support@flndrn.com';

function ticketBlock(ticketNumber: string): string {
  return `<p style="background:#1a1d24;border-radius:8px;padding:12px;font-family:ui-monospace,SFMono-Regular,monospace;font-size:13px;color:#9ba3af;border:1px solid #2a2e36">
      ticket: <strong style="color:#f5f7fa">${escapeHtml(ticketNumber)}</strong>
    </p>`;
}

function ticketCreatedHtml(ticketNumber: string): string {
  const domain = env.BRIVEN_DOMAIN ?? 'briven.tech';
  return shell(
    'we got your support request',
    `
    <p>thanks for reaching out. your message is now a support ticket and we'll get back to you by email.</p>
    ${ticketBlock(ticketNumber)}
    ${cta('view your tickets', `https://${domain}/dashboard/support`)}
    <p class="muted">quote your ticket number above in any follow-up, or write to ${SUPPORT_CONTACT}.</p>
  `,
  );
}

function ticketCreatedText(ticketNumber: string): string {
  const domain = env.BRIVEN_DOMAIN ?? 'briven.tech';
  return [
    'we got your support request',
    '',
    `thanks for reaching out. your message is now a support ticket: ${ticketNumber}`,
    '',
    `view your tickets: https://${domain}/dashboard/support`,
    '',
    `quote your ticket number in any follow-up, or write to ${SUPPORT_CONTACT}.`,
  ].join('\n');
}

function ticketReplyHtml(ticketNumber: string, body: string): string {
  const domain = env.BRIVEN_DOMAIN ?? 'briven.tech';
  return shell(
    'a reply on your support request',
    `
    <p>we've replied to your ticket:</p>
    <p style="background:#1a1d24;border-radius:8px;padding:12px;white-space:pre-wrap;color:#d1d5db;border:1px solid #2a2e36;font-size:14px">${escapeHtml(body)}</p>
    ${ticketBlock(ticketNumber)}
    ${cta('open the ticket', `https://${domain}/dashboard/support`)}
    <p class="muted">reply to this email or write to ${SUPPORT_CONTACT} and quote the ticket number above.</p>
  `,
  );
}

function ticketReplyText(ticketNumber: string, body: string): string {
  const domain = env.BRIVEN_DOMAIN ?? 'briven.tech';
  return [
    'a reply on your support request',
    '',
    body,
    '',
    `ticket: ${ticketNumber}`,
    '',
    `open the ticket: https://${domain}/dashboard/support`,
    '',
    `reply to this email or write to ${SUPPORT_CONTACT} and quote the ticket number.`,
  ].join('\n');
}

function contactReplyHtml(body: string): string {
  return shell(
    'a reply to your message',
    `
    <p>we've replied to the message you sent us:</p>
    <p style="background:#1a1d24;border-radius:8px;padding:12px;white-space:pre-wrap;color:#d1d5db;border:1px solid #2a2e36;font-size:14px">${escapeHtml(body)}</p>
    <p class="muted">reply to this email or write to ${SUPPORT_CONTACT} and we'll pick it up from there.</p>
  `,
  );
}

function contactReplyText(body: string): string {
  return [
    'a reply to your message',
    '',
    body,
    '',
    `reply to this email or write to ${SUPPORT_CONTACT} and we'll pick it up from there.`,
  ].join('\n');
}

function ticketStatusBlurb(status: string): string {
  switch (status) {
    case 'replied':
      return 'we’ve replied to your ticket — check the thread for our latest message.';
    case 'closed':
      return 'we’ve marked your ticket as resolved. if you still need help, just reply and it’ll reopen.';
    case 'in_review':
      return 'your ticket is being looked at — we’ll follow up shortly.';
    default:
      return 'your ticket status was updated.';
  }
}

function ticketStatusHtml(ticketNumber: string, status: string): string {
  const domain = env.BRIVEN_DOMAIN ?? 'briven.tech';
  return shell(
    'update on your support request',
    `
    <p>${ticketStatusBlurb(status)}</p>
    ${ticketBlock(ticketNumber)}
    ${cta('open the ticket', `https://${domain}/dashboard/support`)}
    <p class="muted">need a human? reply to this email or write to ${SUPPORT_CONTACT}.</p>
  `,
  );
}

function ticketStatusText(ticketNumber: string, status: string): string {
  const domain = env.BRIVEN_DOMAIN ?? 'briven.tech';
  return [
    'update on your support request',
    '',
    ticketStatusBlurb(status),
    '',
    `ticket: ${ticketNumber}`,
    '',
    `open the ticket: https://${domain}/dashboard/support`,
    '',
    `need a human? reply to this email or write to ${SUPPORT_CONTACT}.`,
  ].join('\n');
}
