import process from 'node:process'
import path from 'node:path'
import net from 'node:net'
import { appendFile, mkdir, writeFile } from 'node:fs/promises'

function parseArgs(argv) {
  const args = new Map()

  for (let index = 0; index < argv.length; index += 1) {
    const value = argv[index]
    if (!value.startsWith('--')) {
      continue
    }

    const key = value.slice(2)
    const nextValue = argv[index + 1]
    if (nextValue && !nextValue.startsWith('--')) {
      args.set(key, nextValue)
      index += 1
      continue
    }

    args.set(key, 'true')
  }

  return args
}

const args = parseArgs(process.argv.slice(2))
const port = Number(args.get('port') ?? process.env.SMTP_CAPTURE_PORT ?? 2525)
const outputPath = path.resolve(args.get('output') ?? process.env.SMTP_CAPTURE_OUTPUT ?? './smtp-capture.jsonl')

if (!Number.isInteger(port) || port <= 0) {
  throw new Error(`Invalid SMTP capture port: ${port}`)
}

await mkdir(path.dirname(outputPath), { recursive: true })
await writeFile(outputPath, '', 'utf8')

let writeQueue = Promise.resolve()

function queueMessageWrite(message) {
  writeQueue = writeQueue.then(() => appendFile(outputPath, `${JSON.stringify(message)}\n`, 'utf8'))
  return writeQueue
}

function createSessionState() {
  return {
    buffer: '',
    dataMode: false,
    mailFrom: '',
    rcptTo: [],
    data: '',
  }
}

const server = net.createServer((socket) => {
  socket.setEncoding('utf8')
  let session = createSessionState()

  const reply = (line) => {
    socket.write(`${line}\r\n`)
  }

  const resetMessageState = () => {
    session.dataMode = false
    session.mailFrom = ''
    session.rcptTo = []
    session.data = ''
  }

  const flushBuffer = async () => {
    while (true) {
      if (session.dataMode) {
        const messageTerminatorIndex = session.buffer.indexOf('\r\n.\r\n')
        if (messageTerminatorIndex === -1) {
          session.data += session.buffer
          session.buffer = ''
          return
        }

        session.data += session.buffer.slice(0, messageTerminatorIndex)
        session.buffer = session.buffer.slice(messageTerminatorIndex + 5)

        const capturedMessage = {
          timestamp: new Date().toISOString(),
          mailFrom: session.mailFrom,
          rcptTo: session.rcptTo,
          data: session.data.replace(/\r\n\.\./g, '\r\n.'),
        }

        await queueMessageWrite(capturedMessage)
        resetMessageState()
        reply('250 OK')
        continue
      }

      const lineEndIndex = session.buffer.indexOf('\r\n')
      if (lineEndIndex === -1) {
        return
      }

      const line = session.buffer.slice(0, lineEndIndex)
      session.buffer = session.buffer.slice(lineEndIndex + 2)
      const normalized = line.toUpperCase()

      if (normalized.startsWith('EHLO')) {
        socket.write('250-localhost\r\n250 OK\r\n')
        continue
      }

      if (normalized.startsWith('HELO')) {
        reply('250 OK')
        continue
      }

      if (normalized.startsWith('MAIL FROM:')) {
        resetMessageState()
        session.mailFrom = line.slice('MAIL FROM:'.length).trim()
        reply('250 OK')
        continue
      }

      if (normalized.startsWith('RCPT TO:')) {
        session.rcptTo.push(line.slice('RCPT TO:'.length).trim())
        reply('250 OK')
        continue
      }

      if (normalized === 'DATA') {
        session.dataMode = true
        session.data = ''
        reply('354 End data with <CR><LF>.<CR><LF>')
        continue
      }

      if (normalized === 'RSET') {
        resetMessageState()
        reply('250 OK')
        continue
      }

      if (normalized === 'NOOP') {
        reply('250 OK')
        continue
      }

      if (normalized === 'QUIT') {
        reply('221 Bye')
        socket.end()
        return
      }

      reply('250 OK')
    }
  }

  socket.on('data', (chunk) => {
    session.buffer += chunk
    void flushBuffer().catch((error) => {
      console.error(error?.stack ?? String(error))
      socket.destroy(error)
    })
  })

  socket.on('error', () => {})
  reply('220 localhost ESMTP ready')
})

server.listen(port, '127.0.0.1', () => {
  console.log(`SMTP capture listening on 127.0.0.1:${port}`)
})

async function shutdown() {
  server.close()
  await writeQueue.catch(() => {})
}

process.on('SIGINT', () => {
  void shutdown().finally(() => process.exit(0))
})

process.on('SIGTERM', () => {
  void shutdown().finally(() => process.exit(0))
})
