import net from "node:net";
import tls from "node:tls";
import { randomUUID } from "node:crypto";
import { isValidEmail } from "./security";

export type SmtpSendResult = { ok: boolean; skipped?: boolean; status: string; error?: string };
export type SmtpProfile = "default" | "guides" | "enquiries";

export type MailMessage = {
  to: string | string[];
  cc?: string | string[];
  bcc?: string | string[];
  subject: string;
  text: string;
  replyTo?: string;
  fromEmail?: string;
  fromName?: string;
};

type SmtpSocket = net.Socket | tls.TLSSocket;

function envBool(value: string | undefined, fallback = false) {
  if (value == null || value === "") return fallback;
  return ["1", "true", "yes", "on"].includes(value.toLowerCase());
}


function profilePrefixes(profile: SmtpProfile) {
  if (profile === "guides") return ["MPC_GUIDES_SMTP", "MPC_GUIDE_SMTP", "MPC_SMTP"];
  if (profile === "enquiries") return ["MPC_ENQUIRY_SMTP", "MPC_ENQUIRIES_SMTP", "MPC_SMTP"];
  return ["MPC_SMTP"];
}

function profileLabel(profile: SmtpProfile) {
  if (profile === "guides") return "Guides SMTP profile";
  if (profile === "enquiries") return "Enquiries SMTP profile";
  return "Default SMTP profile";
}

function profileEnv(profile: SmtpProfile, key: string) {
  for (const prefix of profilePrefixes(profile)) {
    const value = process.env[`${prefix}_${key}`];
    if (value != null && value !== "") return value;
  }
  return "";
}

function smtpProfileConfig(profile: SmtpProfile) {
  const host = profileEnv(profile, "HOST");
  const user = profileEnv(profile, "USER");
  const pass = profileEnv(profile, "PASS");
  const envPort = Number(profileEnv(profile, "PORT") || 0);
  const secure = envBool(profileEnv(profile, "SECURE"), envPort === 465);
  const port = envPort || (secure ? 465 : 587);
  const from = profileEnv(profile, "FROM");
  const fromName = profileEnv(profile, "FROM_NAME");
  const rejectUnauthorizedValue = profileEnv(profile, "REJECT_UNAUTHORIZED");
  const ehloDomain = profileEnv(profile, "EHLO_DOMAIN") || process.env.MPC_SMTP_EHLO_DOMAIN || "marlothparkcentral.com";
  return { host, user, pass, port, secure, from, fromName, rejectUnauthorizedValue, ehloDomain };
}

export function smtpProfileStatus(profile: SmtpProfile) {
  const config = smtpProfileConfig(profile);
  if (config.host && config.user && config.pass) return `${profileLabel(profile)} configured as ${config.user} via ${config.host}:${config.port}.`;
  return `${profileLabel(profile)} not fully configured. Set ${profile === "guides" ? "MPC_GUIDES_SMTP_*" : profile === "enquiries" ? "MPC_ENQUIRY_SMTP_*" : "MPC_SMTP_*"} variables, or keep MPC_SMTP_* as a shared fallback.`;
}

function uniq(values: string[]) {
  const seen = new Set<string>();
  return values.filter((item) => {
    const key = item.trim().toLowerCase();
    if (!key || seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

export function emailList(input?: string | string[] | null) {
  const raw = Array.isArray(input) ? input.join(",") : String(input || "");
  return uniq(raw.split(/[;,]/).map((item) => extractEmailAddress(item)).filter((item) => isValidEmail(item)));
}

export function extractEmailAddress(value: string) {
  const clean = String(value || "").replace(/[\r\n<>]/g, " ").trim();
  const angle = String(value || "").match(/<([^>]+)>/);
  const candidate = (angle?.[1] || clean).replace(/[\r\n<>]/g, " ").trim();
  return isValidEmail(candidate) ? candidate : "";
}

function encodeHeader(value: string) {
  if (/^[\x20-\x7E]*$/.test(value)) return value.replace(/[\r\n]+/g, " ");
  return `=?UTF-8?B?${Buffer.from(value.replace(/[\r\n]+/g, " "), "utf8").toString("base64")}?=`;
}

function formatFrom(name: string, email: string) {
  const safeName = name.replace(/[\r\n"]/g, " ").trim();
  return safeName ? `"${safeName}" <${email}>` : email;
}

function normalizeLineEndings(value: string) {
  return value.replace(/\r?\n/g, "\r\n").replace(/^\./gm, "..");
}

function buildMessage(message: MailMessage, allVisibleRecipients: string[], fromEmail: string, fromName: string) {
  const headers = [
    `From: ${formatFrom(fromName, fromEmail)}`,
    `To: ${allVisibleRecipients.join(", ")}`,
    message.cc ? `Cc: ${emailList(message.cc).join(", ")}` : "",
    message.replyTo ? `Reply-To: ${extractEmailAddress(message.replyTo)}` : "",
    `Subject: ${encodeHeader(message.subject)}`,
    `Date: ${new Date().toUTCString()}`,
    `Message-ID: <${randomUUID()}@marlothparkcentral.com>`,
    "MIME-Version: 1.0",
    'Content-Type: text/plain; charset="UTF-8"',
    "Content-Transfer-Encoding: 8bit",
  ].filter(Boolean);
  return `${headers.join("\r\n")}\r\n\r\n${normalizeLineEndings(message.text)}\r\n`;
}

function waitForConnect(socket: SmtpSocket) {
  return new Promise<void>((resolve, reject) => {
    if ((socket as any).connecting === false) return resolve();
    socket.once("connect", () => resolve());
    socket.once("secureConnect", () => resolve());
    socket.once("error", reject);
  });
}

function createResponseReader(socketRef: { current: SmtpSocket }) {
  let buffer = "";
  const waiters: Array<{ resolve: (value: string) => void; reject: (reason?: unknown) => void; timer: NodeJS.Timeout }> = [];

  function attach(socket: SmtpSocket) {
    socket.on("data", (chunk) => {
      buffer += chunk.toString("utf8");
      flush();
    });
    socket.on("error", (error) => {
      while (waiters.length) {
        const waiter = waiters.shift()!;
        clearTimeout(waiter.timer);
        waiter.reject(error);
      }
    });
  }

  function extract() {
    const lines = buffer.split(/\r?\n/);
    if (!buffer.endsWith("\n")) lines.pop();
    let consumedLines = 0;
    for (let index = 0; index < lines.length; index += 1) {
      const line = lines[index];
      consumedLines += 1;
      if (/^\d{3} /.test(line)) {
        const response = lines.slice(0, index + 1).join("\n");
        buffer = lines.slice(index + 1).join("\n");
        if (buffer) buffer += "\n";
        return response;
      }
    }
    if (consumedLines && lines.length === consumedLines && buffer.endsWith("\n")) {
      buffer = lines.join("\n");
    }
    return null;
  }

  function flush() {
    while (waiters.length) {
      const response = extract();
      if (!response) break;
      const waiter = waiters.shift()!;
      clearTimeout(waiter.timer);
      waiter.resolve(response);
    }
  }

  function read(timeoutMs = 15000) {
    const existing = extract();
    if (existing) return Promise.resolve(existing);
    return new Promise<string>((resolve, reject) => {
      const timer = setTimeout(() => {
        const idx = waiters.findIndex((waiter) => waiter.resolve === resolve);
        if (idx >= 0) waiters.splice(idx, 1);
        reject(new Error("SMTP response timed out"));
      }, timeoutMs);
      waiters.push({ resolve, reject, timer });
    });
  }

  function reattach(socket: SmtpSocket) {
    buffer = "";
    attach(socket);
  }

  attach(socketRef.current);
  return { read, reattach };
}

function responseCode(response: string) {
  const match = response.match(/^(\d{3})/m);
  return match ? Number(match[1]) : 0;
}

async function writeCommand(socket: SmtpSocket, command: string) {
  await new Promise<void>((resolve, reject) => {
    socket.write(`${command}\r\n`, (error) => (error ? reject(error) : resolve()));
  });
}

async function expect(reader: ReturnType<typeof createResponseReader>, okCodes: number[], context: string) {
  const response = await reader.read();
  const code = responseCode(response);
  if (!okCodes.includes(code)) throw new Error(`${context} failed: ${response.replace(/\s+/g, " ").slice(0, 240)}`);
  return response;
}

function isSmtpCertificateMismatch(message: string) {
  return /Hostname\/IP does not match certificate|ERR_TLS_CERT_ALTNAME_INVALID|certificate's altnames|cert.*altname|unable to verify|self-signed certificate/i.test(String(message || ""));
}

export function smtpConfigured(profile: SmtpProfile = "default") {
  const config = smtpProfileConfig(profile);
  return Boolean(config.host && config.user && config.pass);
}

export async function sendSmtpMail(message: MailMessage, options: { profile?: SmtpProfile } = {}): Promise<SmtpSendResult> {
  const profile = options.profile || "default";
  const config = smtpProfileConfig(profile);
  const host = config.host;
  const user = config.user;
  const pass = config.pass;
  const secure = config.secure;
  const port = config.port;
  const fromEmail = extractEmailAddress(message.fromEmail || config.from || user || "");
  const fromName = message.fromName || config.fromName || "Marloth Park Central";
  const configuredRejectUnauthorized = !envBool(config.rejectUnauthorizedValue, true) ? false : true;

  const to = emailList(message.to);
  const cc = emailList(message.cc);
  const bcc = emailList(message.bcc);
  const recipients = uniq([...to, ...cc, ...bcc].map(extractEmailAddress));

  if (!host || !user || !pass || !fromEmail) {
    return { ok: false, skipped: true, status: `${profileLabel(profile)} not configured. Set ${profile === "guides" ? "MPC_GUIDES_SMTP_HOST, MPC_GUIDES_SMTP_PORT, MPC_GUIDES_SMTP_USER, MPC_GUIDES_SMTP_PASS and MPC_GUIDES_SMTP_FROM" : profile === "enquiries" ? "MPC_ENQUIRY_SMTP_HOST, MPC_ENQUIRY_SMTP_PORT, MPC_ENQUIRY_SMTP_USER, MPC_ENQUIRY_SMTP_PASS and MPC_ENQUIRY_SMTP_FROM" : "MPC_SMTP_HOST, MPC_SMTP_PORT, MPC_SMTP_USER, MPC_SMTP_PASS and MPC_SMTP_FROM"}. Shared MPC_SMTP_* values are used as fallback when profile-specific values are missing.` };
  }
  if (!isValidEmail(fromEmail)) return { ok: false, skipped: true, status: "SMTP sender email is invalid." };
  if (!recipients.length) return { ok: false, skipped: true, status: "No email recipients supplied." };

  async function attemptSend(rejectUnauthorized: boolean): Promise<SmtpSendResult> {
    const socketRef: { current: SmtpSocket } = {
      current: secure
        ? tls.connect({ host, port, servername: host, rejectUnauthorized })
        : net.connect({ host, port }),
    };
    const reader = createResponseReader(socketRef);

    try {
      await waitForConnect(socketRef.current);
      await expect(reader, [220], "SMTP greeting");
      await writeCommand(socketRef.current, `EHLO ${config.ehloDomain}`);
      const ehlo = await expect(reader, [250], "SMTP EHLO");

      if (!secure && /STARTTLS/i.test(ehlo)) {
        await writeCommand(socketRef.current, "STARTTLS");
        await expect(reader, [220], "SMTP STARTTLS");
        socketRef.current.removeAllListeners("data");
        socketRef.current = tls.connect({ socket: socketRef.current, servername: host, rejectUnauthorized });
        await waitForConnect(socketRef.current);
        reader.reattach(socketRef.current);
        await writeCommand(socketRef.current, `EHLO ${config.ehloDomain}`);
        await expect(reader, [250], "SMTP EHLO after STARTTLS");
      }

      await writeCommand(socketRef.current, "AUTH LOGIN");
      await expect(reader, [334], "SMTP AUTH LOGIN");
      await writeCommand(socketRef.current, Buffer.from(user).toString("base64"));
      await expect(reader, [334], "SMTP AUTH username");
      await writeCommand(socketRef.current, Buffer.from(pass).toString("base64"));
      await expect(reader, [235], "SMTP AUTH password");

      await writeCommand(socketRef.current, `MAIL FROM:<${fromEmail}>`);
      await expect(reader, [250], "SMTP MAIL FROM");
      for (const recipient of recipients) {
        await writeCommand(socketRef.current, `RCPT TO:<${recipient}>`);
        await expect(reader, [250, 251], `SMTP RCPT TO ${recipient}`);
      }
      await writeCommand(socketRef.current, "DATA");
      await expect(reader, [354], "SMTP DATA");
      const rawMessage = buildMessage(message, to, fromEmail, fromName);
      await new Promise<void>((resolve, reject) => {
        socketRef.current.write(`${rawMessage}\r\n.\r\n`, (error) => (error ? reject(error) : resolve()));
      });
      await expect(reader, [250], "SMTP message body");
      await writeCommand(socketRef.current, "QUIT");
      try { await reader.read(4000); } catch {}
      socketRef.current.end();
      return { ok: true, status: `${profileLabel(profile)} sent email to ${recipients.join(", ")}${rejectUnauthorized ? "" : " with relaxed TLS certificate verification for the cPanel mail host"}` };
    } catch (error) {
      socketRef.current.destroy();
      return { ok: false, status: error instanceof Error ? error.message : "SMTP send failed", error: error instanceof Error ? error.message : "SMTP send failed" };
    }
  }

  const first = await attemptSend(configuredRejectUnauthorized);
  if (first.ok || !configuredRejectUnauthorized || !isSmtpCertificateMismatch(first.error || first.status)) return first;

  const retry = await attemptSend(false);
  if (retry.ok) {
    return {
      ok: true,
      status: `${retry.status}. The first strict TLS attempt failed because the SMTP certificate did not match ${host}; v6.82.89 retried safely for cPanel delivery. Long-term fix: set the SMTP host to the provider hostname that matches the certificate, or set MPC_ENQUIRY_SMTP_REJECT_UNAUTHORIZED=false.`,
    };
  }

  return {
    ok: false,
    status: `${first.status}. Retried with relaxed TLS certificate verification, but delivery still failed: ${retry.status}`,
    error: retry.error || first.error,
  };
}
