<?php

declare(strict_types=1);

namespace BotGuard\Services\Mail;

use RuntimeException;

/**
 * Dependency-free SMTP client.
 *
 * Speaks enough of RFC 5321/5322 to deliver a MIME (plain + HTML) email through
 * an authenticated SMTP server, supporting implicit TLS (smtps / port 465) and
 * STARTTLS (port 587), with AUTH LOGIN / PLAIN. No PHPMailer/Symfony Mailer
 * dependency — consistent with the rest of the platform.
 */
final class SmtpMailer
{
    private mixed $socket = null;

    /**
     * @param array{
     *   host:string, port:int, encryption:string, username:string, password:string,
     *   from_address:string, from_name:string, timeout?:int
     * } $config
     */
    public function __construct(private array $config)
    {
    }

    /**
     * Send an email. Throws on failure with a descriptive message.
     */
    public function send(string $toEmail, string $subject, string $html, string $text = ''): bool
    {
        $host = (string) $this->config['host'];
        $port = (int) $this->config['port'];
        $encryption = strtolower((string) ($this->config['encryption'] ?? 'tls'));
        $timeout = (int) ($this->config['timeout'] ?? 15);

        if ($host === '') {
            throw new RuntimeException('SMTP host is not configured.');
        }

        $remote = $encryption === 'ssl' ? "ssl://{$host}:{$port}" : "tcp://{$host}:{$port}";
        $context = stream_context_create(['ssl' => ['verify_peer' => true, 'verify_peer_name' => true]]);

        $this->socket = @stream_socket_client($remote, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $context);
        if ($this->socket === false) {
            throw new RuntimeException("SMTP connection failed: {$errstr} ({$errno})");
        }
        stream_set_timeout($this->socket, $timeout);

        try {
            $this->expect(220);

            $ehloHost = gethostname() ?: 'localhost';
            $this->command("EHLO {$ehloHost}", 250);

            if ($encryption === 'tls') {
                $this->command('STARTTLS', 220);
                if (! stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT)) {
                    throw new RuntimeException('Failed to establish TLS with the SMTP server.');
                }
                $this->command("EHLO {$ehloHost}", 250);
            }

            $this->authenticate();

            $from = (string) $this->config['from_address'];
            $this->command("MAIL FROM:<{$from}>", 250);
            $this->command("RCPT TO:<{$toEmail}>", [250, 251]);
            $this->command('DATA', 354);

            $this->write($this->buildMessage($toEmail, $subject, $html, $text) . "\r\n.");
            $this->expect(250);

            $this->command('QUIT', 221);

            return true;
        } finally {
            if (is_resource($this->socket)) {
                fclose($this->socket);
            }
        }
    }

    private function authenticate(): void
    {
        $username = (string) $this->config['username'];
        $password = (string) $this->config['password'];

        if ($username === '') {
            return; // unauthenticated relay
        }

        // Prefer AUTH LOGIN (widely supported); fall back to PLAIN.
        try {
            $this->command('AUTH LOGIN', 334);
            $this->command(base64_encode($username), 334);
            $this->command(base64_encode($password), 235);
        } catch (RuntimeException) {
            $plain = base64_encode("\0{$username}\0{$password}");
            $this->command("AUTH PLAIN {$plain}", 235);
        }
    }

    /**
     * Assemble the RFC 5322 message with a multipart/alternative body.
     */
    private function buildMessage(string $toEmail, string $subject, string $html, string $text): string
    {
        $fromAddress = (string) $this->config['from_address'];
        $fromName = (string) ($this->config['from_name'] ?? $fromAddress);
        $boundary = 'bg-' . bin2hex(random_bytes(12));
        $text = $text !== '' ? $text : trim(strip_tags($html));

        $headers = [
            'Date: ' . date('r'),
            'From: ' . $this->encodeHeader($fromName) . " <{$fromAddress}>",
            "To: <{$toEmail}>",
            'Subject: ' . $this->encodeHeader($subject),
            'Message-ID: <' . bin2hex(random_bytes(16)) . "@{$this->hostFromEmail($fromAddress)}>",
            'MIME-Version: 1.0',
            "Content-Type: multipart/alternative; boundary=\"{$boundary}\"",
        ];

        $body = [
            "--{$boundary}",
            'Content-Type: text/plain; charset=UTF-8',
            'Content-Transfer-Encoding: base64',
            '',
            chunk_split(base64_encode($text)),
            "--{$boundary}",
            'Content-Type: text/html; charset=UTF-8',
            'Content-Transfer-Encoding: base64',
            '',
            chunk_split(base64_encode($html)),
            "--{$boundary}--",
        ];

        return implode("\r\n", $headers) . "\r\n\r\n" . implode("\r\n", $body);
    }

    /**
     * Send a command and assert the expected reply code(s).
     *
     * @param int|int[] $expected
     */
    private function command(string $command, int|array $expected): void
    {
        $this->write($command);
        $this->expect($expected);
    }

    private function write(string $data): void
    {
        if (! is_resource($this->socket) || fwrite($this->socket, $data . "\r\n") === false) {
            throw new RuntimeException('Failed writing to the SMTP socket.');
        }
    }

    /**
     * Read a (possibly multi-line) reply and verify its code.
     *
     * @param int|int[] $expected
     */
    private function expect(int|array $expected): string
    {
        $expected = (array) $expected;
        $response = '';

        while (is_resource($this->socket) && ($line = fgets($this->socket, 515)) !== false) {
            $response .= $line;
            // Continuation lines look like "250-...", the final line "250 ...".
            if (isset($line[3]) && $line[3] === ' ') {
                break;
            }
        }

        $code = (int) substr($response, 0, 3);
        if (! in_array($code, $expected, true)) {
            throw new RuntimeException('Unexpected SMTP reply: ' . trim($response));
        }

        return $response;
    }

    private function encodeHeader(string $value): string
    {
        return preg_match('/[^\x20-\x7e]/', $value)
            ? '=?UTF-8?B?' . base64_encode($value) . '?='
            : $value;
    }

    private function hostFromEmail(string $email): string
    {
        $parts = explode('@', $email);

        return $parts[1] ?? 'localhost';
    }
}
