import { accessSync, constants } from "node:fs";
import nodemailer, { type Transporter } from "nodemailer";
import { CONTACT_SUBJECTS, type ContactSubject } from "@/lib/contact-subjects";

/**
 * Contact-form mail plumbing.
 *
 * Config reading, payload validation and message building are pure functions so
 * they can be unit-tested without a network or a running server. The transport
 * itself is created lazily — importing this module on a box with no SMTP config
 * must not throw, otherwise the whole route fails to load.
 */

export { CONTACT_SUBJECTS, type ContactSubject };

/**
 * Two ways to hand a message off, chosen by MAIL_TRANSPORT:
 *
 *   sendmail — pipe to the local msmtp (the default, and what the servers use).
 *              msmtp already holds the Titan credentials on every host, so the
 *              app itself stores no password at all and a credential rotation
 *              never touches this deployment.
 *   smtp     — talk to a mail server directly. Useful locally, and the fallback
 *              for a host with no local relay.
 *
 * With MAIL_TRANSPORT unset the presence of SMTP_HOST decides, so an existing
 * SMTP-configured box keeps working untouched.
 */
export type TransportKind = "sendmail" | "smtp";

/** Needed whichever transport is in use. */
export const REQUIRED_MAIL_ENV = ["SMTP_FROM", "CONTACT_TO"] as const;

/** Needed on top of the above when talking to a mail server directly. */
export const REQUIRED_SMTP_ENV = ["SMTP_HOST", "SMTP_USER", "SMTP_PASS"] as const;

export type SendmailConfig = {
  transport: "sendmail";
  path: string;
  args: string[];
  from: string;
  to: string;
};

export type SmtpConfig = {
  transport: "smtp";
  host: string;
  port: number;
  secure: boolean;
  user: string;
  pass: string;
  from: string;
  to: string;
  rejectUnauthorized: boolean;
};

export type MailConfig = SendmailConfig | SmtpConfig;

export type MailConfigResult =
  | { ok: true; config: MailConfig }
  | { ok: false; transport: TransportKind; missing: string[]; invalid: string[] };

const DEFAULT_PORT = 465;
const DEFAULT_SENDMAIL_PATH = "/usr/sbin/sendmail";

function boolFromEnv(raw: string | undefined, fallback: boolean): boolean {
  if (raw === undefined || raw.trim() === "") return fallback;
  return !/^(false|0|no|off)$/i.test(raw.trim());
}

/**
 * Read and validate SMTP settings from the environment.
 *
 * Never throws — callers get the list of missing/invalid vars so the failure can
 * be logged and surfaced as a 503 instead of a mystery 500 thirty seconds later.
 */
export function resolveTransportKind(env: NodeJS.ProcessEnv = process.env): TransportKind {
  const explicit = env.MAIL_TRANSPORT?.trim().toLowerCase();
  if (explicit === "sendmail" || explicit === "smtp") return explicit;
  return env.SMTP_HOST?.trim() ? "smtp" : "sendmail";
}

export function readMailConfig(env: NodeJS.ProcessEnv = process.env): MailConfigResult {
  const transport = resolveTransportKind(env);
  const missing: string[] = REQUIRED_MAIL_ENV.filter((key) => !env[key]?.trim());
  const invalid: string[] = [];

  if (transport === "sendmail") {
    if (missing.length > 0) return { ok: false, transport, missing, invalid };
    return {
      ok: true,
      config: {
        transport,
        path: env.SENDMAIL_PATH?.trim() || DEFAULT_SENDMAIL_PATH,
        // Extra flags for the local relay, e.g. SENDMAIL_ARGS="-a clearfin"
        // when msmtprc defines more than one account.
        args: env.SENDMAIL_ARGS?.trim() ? env.SENDMAIL_ARGS.trim().split(/\s+/) : [],
        from: env.SMTP_FROM!.trim(),
        to: env.CONTACT_TO!.trim(),
      },
    };
  }

  missing.push(...REQUIRED_SMTP_ENV.filter((key) => !env[key]?.trim()));

  const rawPort = env.SMTP_PORT?.trim();
  const port = rawPort ? Number.parseInt(rawPort, 10) : DEFAULT_PORT;
  if (!Number.isInteger(port) || port < 1 || port > 65535) invalid.push("SMTP_PORT");

  if (missing.length > 0 || invalid.length > 0) return { ok: false, transport, missing, invalid };

  return {
    ok: true,
    config: {
      transport,
      host: env.SMTP_HOST!.trim(),
      port,
      // Implicit TLS is port 465; anything else (587, 2525) starts plain and
      // upgrades via STARTTLS. SMTP_SECURE overrides when a provider differs.
      secure: boolFromEnv(env.SMTP_SECURE, port === 465),
      user: env.SMTP_USER!.trim(),
      pass: env.SMTP_PASS!,
      from: env.SMTP_FROM!.trim(),
      to: env.CONTACT_TO!.trim(),
      // Certificate validation stays on unless explicitly disabled — the local
      // fake-SMTP server in the test suite is the only thing that needs it off.
      rejectUnauthorized: boolFromEnv(env.SMTP_TLS_REJECT_UNAUTHORIZED, true),
    },
  };
}

let cachedTransport: { key: string; transport: Transporter } | null = null;

/** Lazily create (and reuse) the transport for a given config. */
export function getMailTransport(config: MailConfig): Transporter {
  const key = JSON.stringify(config);
  if (cachedTransport?.key === key) return cachedTransport.transport;

  const transport =
    config.transport === "sendmail"
      ? nodemailer.createTransport({
          sendmail: true,
          path: config.path,
          // nodemailer prepends -i and appends the recipients, giving
          // `-i <args> -f <from> <to>` — the envelope sender msmtp expects.
          args: [...config.args, "-f", config.from],
        })
      : nodemailer.createTransport({
          host: config.host,
          port: config.port,
          secure: config.secure,
          auth: { user: config.user, pass: config.pass },
          tls: { rejectUnauthorized: config.rejectUnauthorized },
          // Without these a black-holed SMTP port hangs the request for minutes.
          connectionTimeout: 10_000,
          greetingTimeout: 10_000,
          socketTimeout: 20_000,
        });

  cachedTransport = { key, transport };
  return transport;
}

/** Test seam: drop the memoised transport so a new config takes effect. */
export function resetMailTransport(): void {
  cachedTransport = null;
}

export type MailReadiness = { ok: true } | { ok: false; problem: string };

/**
 * Cheap local check that the configured transport could actually be used.
 *
 * For the sendmail pipe this catches the two ways it fails on a real box — the
 * binary is missing, or the service user cannot execute it — at deploy time
 * rather than on the first visitor submission. Nothing is sent, and no
 * connection is opened for the SMTP transport.
 */
export function checkTransportReady(config: MailConfig): MailReadiness {
  if (config.transport !== "sendmail") return { ok: true };

  try {
    accessSync(config.path, constants.X_OK);
    return { ok: true };
  } catch {
    return {
      ok: false,
      problem: `${config.path} is missing or not executable by this service. ` +
        `Install msmtp-mta (or set SENDMAIL_PATH), or set MAIL_TRANSPORT=smtp.`,
    };
  }
}

export type ContactPayload = {
  name: string;
  email: string;
  phone: string;
  company: string;
  subject: string;
  message: string;
};

export type ValidationResult =
  | { ok: true; value: ContactPayload }
  | { ok: false; errors: string[] };

const LIMITS = {
  name: 200,
  email: 320,
  phone: 50,
  company: 200,
  message: 5000,
} as const;

// Deliberately loose: real-world addresses are stranger than any regex, and the
// authoritative check is whether the reply bounces.
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

function asTrimmedString(value: unknown): string {
  return typeof value === "string" ? value.trim() : "";
}

export function validateContactPayload(body: unknown): ValidationResult {
  const errors: string[] = [];
  if (typeof body !== "object" || body === null) {
    return { ok: false, errors: ["Malformed request body."] };
  }

  const raw = body as Record<string, unknown>;
  const name = asTrimmedString(raw.name);
  const email = asTrimmedString(raw.email);
  const phone = asTrimmedString(raw.phone);
  const company = asTrimmedString(raw.company);
  const subject = asTrimmedString(raw.subject);
  const message = asTrimmedString(raw.message);

  if (!name) errors.push("Name is required.");
  else if (name.length > LIMITS.name) errors.push("Name is too long.");

  if (!email) errors.push("Email is required.");
  else if (email.length > LIMITS.email || !EMAIL_RE.test(email)) {
    errors.push("Email is not a valid address.");
  }

  if (phone.length > LIMITS.phone) errors.push("Phone is too long.");

  if (!company) errors.push("Company is required.");
  else if (company.length > LIMITS.company) errors.push("Company is too long.");

  if (!subject) errors.push("Subject is required.");
  else if (!CONTACT_SUBJECTS.includes(subject as ContactSubject)) {
    errors.push("Subject is not one of the offered options.");
  }

  if (!message) errors.push("Message is required.");
  else if (message.length > LIMITS.message) errors.push("Message is too long.");

  if (errors.length > 0) return { ok: false, errors };
  return { ok: true, value: { name, email, phone, company, subject, message } };
}

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

/**
 * Strip CR/LF before anything reaches a mail header. Without this, a newline in
 * a submitted field lets the sender append headers of their own (Bcc, etc.).
 */
export function sanitizeHeader(value: string): string {
  return value.replace(/[\r\n]+/g, " ").trim();
}

export type MailMessage = {
  from: string;
  to: string;
  replyTo: string;
  subject: string;
  text: string;
  html: string;
};

export function buildContactMessage(payload: ContactPayload, config: MailConfig): MailMessage {
  const { name, email, phone, company, subject, message } = payload;
  const phoneOrNa = phone || "n/a";

  const row = (label: string, value: string, shaded: boolean) => `
          <tr${shaded ? ' style="background:#F9F5FF;"' : ""}><td style="padding:14px 32px;font-size:12px;text-transform:uppercase;letter-spacing:1px;color:#9061F9;width:120px;">${label}</td><td style="padding:14px 32px;">${value}</td></tr>`;

  return {
    from: `"ClearFin Website" <${config.from}>`,
    to: config.to,
    replyTo: sanitizeHeader(email),
    subject: sanitizeHeader(`[ClearFin] ${subject}: ${name} (${company})`),
    text: [
      `Name:    ${name}`,
      `Email:   ${email}`,
      `Phone:   ${phoneOrNa}`,
      `Company: ${company}`,
      `Subject: ${subject}`,
      ``,
      `Message:`,
      message,
    ].join("\n"),
    html: `
        <table style="font-family:Helvetica,Arial,sans-serif;font-size:15px;color:#1A1A2E;max-width:600px;width:100%;border-collapse:collapse;">
          <tr><td colspan="2" style="background:#18112E;padding:28px 32px;">
            <span style="font-size:20px;font-weight:300;color:#E9D5FF;">Clear</span><span style="font-size:20px;font-weight:900;color:#FDF4FF;">Fin</span>
            <span style="display:block;font-size:11px;color:#9061F9;letter-spacing:2px;margin-top:4px;">NEW WEBSITE INQUIRY</span>
          </td></tr>${row("Name", escapeHtml(name), true)}${row(
            "Email",
            `<a href="mailto:${escapeHtml(email)}" style="color:#7C3AED;">${escapeHtml(email)}</a>`,
            false,
          )}${row("Phone", escapeHtml(phoneOrNa), true)}${row("Company", escapeHtml(company), false)}${row(
            "Subject",
            escapeHtml(subject),
            true,
          )}
          <tr><td colspan="2" style="padding:24px 32px;border-top:1px solid #E9D5FF;">
            <div style="font-size:12px;text-transform:uppercase;letter-spacing:1px;color:#9061F9;margin-bottom:10px;">Message</div>
            <div style="line-height:1.7;white-space:pre-wrap;">${escapeHtml(message)}</div>
          </td></tr>
          <tr><td colspan="2" style="padding:20px 32px;background:#F9F5FF;font-size:12px;color:#9061F9;border-top:1px solid #E9D5FF;">
            Sent from clearfin.ch contact form · ClearFin GmbH, Blegistrasse 7, 6340 Baar, Switzerland
          </td></tr>
        </table>
      `,
  };
}
