import net from "net"
import tls from "tls"

type SmtpConfig = {
  host: string
  port: number
  domain?: string
  username: string
  password: string
  from: string
  starttls?: boolean
  tls?: boolean
  rejectUnauthorized?: boolean
  timeoutMs?: number
}

function b64(input: string) {
  return Buffer.from(input, "utf8").toString("base64")
}

function extractEnvelopeAddress(from: string): string {
  // Prefer address inside angle brackets
  const angle = from.match(/<\s*([^>\s]+)\s*>/)
  if (angle?.[1]) return angle[1]
  // Fallback: address inside parentheses
  const paren = from.match(/\(([^)\s]+@[^)\s]+)\)/)
  if (paren?.[1]) return paren[1]
  // Fallback: first email-like substring
  const email = from.match(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/)
  if (email?.[0]) return email[0]
  // Last resort: use whole string
  return from
}

type SmtpSocket = net.Socket | tls.TLSSocket

type SmtpResponse = { code: number; lines: string[] }

function createSmtpReader(socket: SmtpSocket, timeoutMs: number) {
  let buffer = ""
  let current: SmtpResponse | null = null
  const queue: SmtpResponse[] = []
  let pending:
    | { resolve: (response: SmtpResponse) => void; reject: (error: unknown) => void; timer: ReturnType<typeof setTimeout> }
    | null = null

  const finalize = (response: SmtpResponse) => {
    if (pending) {
      clearTimeout(pending.timer)
      const resolve = pending.resolve
      pending = null
      resolve(response)
      return
    }
    queue.push(response)
  }

  const onData = (data: Buffer) => {
    buffer += data.toString("utf8")
    const lines = buffer.split(/\r?\n/)
    buffer = lines.pop() ?? ""

    for (const line of lines) {
      if (!line) continue
      const match = line.match(/^(\d{3})([ -])\s?(.*)$/)
      if (!match) continue

      const code = Number(match[1])
      const isFinal = match[2] === " "

      if (!current) current = { code, lines: [] }
      current.lines.push(line)

      if (isFinal) {
        const response = current
        current = null
        finalize(response)
      }
    }
  }

  const onError = (error: unknown) => {
    if (pending) {
      clearTimeout(pending.timer)
      const reject = pending.reject
      pending = null
      reject(error)
    }
  }

  socket.on("data", onData)
  socket.on("error", onError)

  const read = () =>
    new Promise<SmtpResponse>((resolve, reject) => {
      const queued = queue.shift()
      if (queued) {
        resolve(queued)
        return
      }
      if (pending) {
        reject(new Error("smtp_concurrent_read"))
        return
      }
      const timer = setTimeout(() => {
        if (!pending) return
        const rejectPending = pending.reject
        pending = null
        rejectPending(new Error("smtp_timeout"))
      }, timeoutMs)
      pending = { resolve, reject, timer }
    })

  const dispose = () => {
    socket.off("data", onData)
    socket.off("error", onError)
    if (pending) {
      clearTimeout(pending.timer)
      pending = null
    }
  }

  return { read, dispose }
}

function isCapability(lines: string[], capability: string) {
  const upper = capability.trim().toUpperCase()
  return lines.some((line) => line.replace(/^(\d{3})([ -])/, "").trim().toUpperCase().startsWith(upper))
}

function assertCode(response: SmtpResponse, expected: number | ((code: number) => boolean), context: string) {
  const ok = typeof expected === "number" ? response.code === expected : expected(response.code)
  if (ok) return
  const details = response.lines.join(" | ")
  throw new Error(`smtp_unexpected_response:${context}:${response.code}:${details}`)
}

async function connectPlain(host: string, port: number, timeoutMs: number) {
  return new Promise<net.Socket>((resolve, reject) => {
    const socket = net.connect(port, host)
    const timer = setTimeout(() => {
      socket.destroy()
      reject(new Error("smtp_connect_timeout"))
    }, timeoutMs)
    socket.once("connect", () => {
      clearTimeout(timer)
      resolve(socket)
    })
    socket.once("error", (e) => {
      clearTimeout(timer)
      reject(e)
    })
  })
}

async function connectTls(host: string, port: number, rejectUnauthorized: boolean, timeoutMs: number) {
  return new Promise<tls.TLSSocket>((resolve, reject) => {
    const socket = tls.connect({ host, port, rejectUnauthorized, servername: host })
    const timer = setTimeout(() => {
      socket.destroy()
      reject(new Error("smtp_connect_timeout"))
    }, timeoutMs)
    socket.once("secureConnect", () => {
      clearTimeout(timer)
      resolve(socket)
    })
    socket.once("error", (e) => {
      clearTimeout(timer)
      reject(e)
    })
  })
}

async function upgradeToStartTls(socket: net.Socket, host: string, rejectUnauthorized: boolean, timeoutMs: number) {
  return new Promise<tls.TLSSocket>((resolve, reject) => {
    const tlsSocket = tls.connect({ socket, servername: host, rejectUnauthorized })
    const timer = setTimeout(() => {
      tlsSocket.destroy()
      reject(new Error("smtp_connect_timeout"))
    }, timeoutMs)
    tlsSocket.once("secureConnect", () => {
      clearTimeout(timer)
      resolve(tlsSocket)
    })
    tlsSocket.once("error", (e) => {
      clearTimeout(timer)
      reject(e)
    })
  })
}

export async function sendSmtpMail(cfg: SmtpConfig, to: string | string[], subject: string, html: string) {
  const timeoutMs = Math.max(1000, cfg.timeoutMs ?? 10000)
  const rejectUnauthorized = cfg.rejectUnauthorized ?? false
  const implicitTls = cfg.tls ?? cfg.port === 465
  const wantsStarttls = cfg.starttls ?? !implicitTls

  const rcpts: string[] = Array.isArray(to)
    ? to
    : String(to)
        .split(/[;,]/)
        .map((s) => s.trim())
        .filter(Boolean)

  let socket: SmtpSocket | null = null
  let reader: ReturnType<typeof createSmtpReader> | null = null

  const sendLine = (line: string) => socket?.write(line + "\r\n")

  const readExpected = async (expected: number | ((code: number) => boolean), context: string) => {
    if (!reader) throw new Error("smtp_reader_not_ready")
    const response = await reader.read()
    assertCode(response, expected, context)
    return response
  }

  try {
    socket = implicitTls
      ? await connectTls(cfg.host, cfg.port, rejectUnauthorized, timeoutMs)
      : await connectPlain(cfg.host, cfg.port, timeoutMs)

    reader = createSmtpReader(socket, timeoutMs)

    await readExpected(220, "greeting")

    const domain = cfg.domain ?? cfg.host
    sendLine(`EHLO ${domain}`)
    let ehlo = await readExpected(250, "ehlo")

    if (!implicitTls && wantsStarttls) {
      const supportsStarttls = isCapability(ehlo.lines, "STARTTLS")
      if (!supportsStarttls) {
        throw new Error("smtp_starttls_not_supported")
      }

      sendLine("STARTTLS")
      await readExpected(220, "starttls")

      reader.dispose()
      const upgraded = await upgradeToStartTls(socket as net.Socket, cfg.host, rejectUnauthorized, timeoutMs)
      socket = upgraded
      reader = createSmtpReader(socket, timeoutMs)

      sendLine(`EHLO ${domain}`)
      ehlo = await readExpected(250, "ehlo_starttls")
    }

    sendLine("AUTH LOGIN")
    await readExpected(334, "auth_login")
    sendLine(b64(cfg.username))
    await readExpected(334, "auth_username")
    sendLine(b64(cfg.password))
    await readExpected(235, "auth_password")

    const envelopeFrom = extractEnvelopeAddress(cfg.from)
    sendLine(`MAIL FROM:<${envelopeFrom}>`)
    await readExpected((code) => Math.floor(code / 100) === 2, "mail_from")

    for (const rcpt of rcpts) {
      sendLine(`RCPT TO:<${rcpt}>`)
      await readExpected((code) => Math.floor(code / 100) === 2, "rcpt_to")
    }

    sendLine("DATA")
    await readExpected(354, "data")

    const headers = [
      `From: ${cfg.from}`,
      `To: ${Array.isArray(to) ? to.join(", ") : to}`,
      `Subject: ${subject}`,
      "MIME-Version: 1.0",
      "Content-Type: text/html; charset=UTF-8",
    ].join("\r\n")

    sendLine(headers + "\r\n\r\n" + html + "\r\n.")
    await readExpected((code) => Math.floor(code / 100) === 2, "message")

    sendLine("QUIT")
    await readExpected(221, "quit")
  } finally {
    reader?.dispose()
    socket?.end()
  }
}
