<?php
declare(strict_types=1);

namespace App\Mailer;

final class SmtpMailer implements MailerInterface
{
    public function __construct(
        private readonly string $host,
        private readonly int $port = 587,
        private readonly string $encryption = 'starttls',
        private readonly string $username = '',
        private readonly string $password = '',
        private readonly string $authMethod = 'login',
        private readonly string $fromEmail = '',
        private readonly string $fromName = 'Quaeriva',
        private readonly float $timeout = 10.0,
    ) {}

    public function send(EmailMessage $message): void
    {
        $scheme = strtolower(trim($this->encryption)) === 'ssl' ? 'ssl://' : 'tcp://';
        $socket = @stream_socket_client($scheme . $this->host . ':' . $this->port, $errorCode, $errorMessage, $this->timeout, STREAM_CLIENT_CONNECT);
        if (!is_resource($socket)) throw new \RuntimeException('SMTP connection failed: ' . $errorMessage);
        stream_set_timeout($socket, (int) $this->timeout);
        try {
            $this->expect($socket, 220);
            $this->command($socket, 'EHLO quaeriva.local', 250);
            if (strtolower(trim($this->encryption)) === 'starttls') {
                $this->command($socket, 'STARTTLS', 220);
                if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) throw new \RuntimeException('SMTP STARTTLS negotiation failed.');
                $this->command($socket, 'EHLO quaeriva.local', 250);
            }
            // Skipping AUTH silently is what turns a blank username into an
            // opaque "550 AUTH is required" from the submission server. Say
            // which field is missing instead; the panel refuses to save this
            // configuration, so reaching here means it was set out of band.
            if ($this->username === '') {
                throw new \RuntimeException('SMTP username is required: the server expects authentication and none can be sent.');
            }
            if ($this->username !== '') {
                if (strtolower(trim($this->authMethod)) === 'plain') {
                    $this->command($socket, 'AUTH PLAIN ' . base64_encode("\0" . $this->username . "\0" . $this->password), 235);
                } else {
                    $this->command($socket, 'AUTH LOGIN', 334);
                    $this->command($socket, base64_encode($this->username), 334);
                    $this->command($socket, base64_encode($this->password), 235);
                }
            }
            $from = trim($this->fromEmail);
            $to = trim($message->to);
            if (!filter_var($from, FILTER_VALIDATE_EMAIL) || !filter_var($to, FILTER_VALIDATE_EMAIL)) throw new \InvalidArgumentException('SMTP sender or recipient is invalid.');
            $this->command($socket, 'MAIL FROM:<' . $from . '>', 250);
            $this->command($socket, 'RCPT TO:<' . $to . '>', 250);
            $this->command($socket, 'DATA', 354);
            $name = trim(preg_replace('/[\r\n]+/', ' ', $this->fromName) ?? $this->fromName);
            $subject = trim(preg_replace('/[\r\n]+/', ' ', $message->subject) ?? $message->subject);
            if (function_exists('mb_encode_mimeheader')) $subject = mb_encode_mimeheader($subject, 'UTF-8');
            $headers = 'From: ' . ($name !== '' ? $name . ' <' . $from . '>' : $from) . "\r\n" . 'To: <' . $to . ">\r\n" . 'Subject: ' . $subject . "\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n";
            $body = preg_replace('/(^|\r?\n)\./', '$1..', str_replace(["\r\n", "\r", "\n"], "\r\n", $message->body)) ?? $message->body;
            fwrite($socket, $headers . $body . "\r\n.\r\n");
            $this->expect($socket, 250);
            $this->command($socket, 'QUIT', 221);
        } finally { fclose($socket); }
    }

    /** @param resource $socket */
    private function command($socket, string $command, int $expected): void
    {
        fwrite($socket, $command . "\r\n");
        $this->expect($socket, $expected);
    }

    /** @param resource $socket */
    private function expect($socket, int $expected): void
    {
        $response = '';
        while (($line = fgets($socket)) !== false) {
            $response .= $line;
            if (strlen($line) < 4 || $line[3] === ' ') break;
        }
        if ($response === '' || (int) substr($response, 0, 3) !== $expected) throw new \RuntimeException('Unexpected SMTP response: ' . trim($response));
    }
}
