<?php
declare(strict_types=1);

/**
 * Minimal SMTP client — no external dependencies.
 * Supports STARTTLS (port 587) and SSL/TLS (port 465).
 * AUTH LOGIN only (standard for shared hosting incl. all-inkl.com).
 */
class SmtpMailer
{
    private string $host;
    private int    $port;
    private string $encryption; // 'starttls' | 'ssl' | 'none'
    private string $user;
    private string $password;
    private string $fromEmail;
    private string $fromName;
    private bool   $verifyTls;  // set smtp_verify_tls: false in config for self-signed certs

    /** @var resource|null */
    private $socket = null;

    public function __construct(array $cfg)
    {
        $this->host       = $cfg['smtp_host']       ?? '';
        $this->port       = (int)($cfg['smtp_port']  ?? 587);
        $this->encryption = strtolower($cfg['smtp_encryption'] ?? 'starttls');
        $this->user       = $cfg['smtp_user']        ?? '';
        $this->password   = $cfg['smtp_password']    ?? '';
        $this->fromEmail  = $cfg['from_email']       ?? '';
        $this->fromName   = $cfg['from_name']        ?? '';
        $this->verifyTls  = (bool)($cfg['smtp_verify_tls'] ?? true);
    }

    /**
     * Send one HTML e-mail. Opens a fresh connection per call so that
     * a single failed recipient does not abort the batch.
     *
     * @throws RuntimeException on SMTP error
     */
    public function send(string $toEmail, string $toName, string $subject, string $html): void
    {
        $this->connect();

        try {
            $hostname = gethostname() ?: 'localhost';

            $this->cmd("EHLO {$hostname}", 250);

            if ($this->encryption === 'starttls') {
                $this->cmd('STARTTLS', 220);

                $cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT;
                if ($this->verifyTls) {
                    // Force peer verification — catches MITM on compromised networks
                    stream_context_set_option($this->socket, 'ssl', 'verify_peer',      true);
                    stream_context_set_option($this->socket, 'ssl', 'verify_peer_name', true);
                    stream_context_set_option($this->socket, 'ssl', 'peer_name',        $this->host);
                } else {
                    stream_context_set_option($this->socket, 'ssl', 'verify_peer',      false);
                    stream_context_set_option($this->socket, 'ssl', 'verify_peer_name', false);
                }

                if (!stream_socket_enable_crypto($this->socket, true, $cryptoMethod)) {
                    throw new RuntimeException('STARTTLS handshake failed');
                }
                $this->cmd("EHLO {$hostname}", 250);
            }

            $this->cmd('AUTH LOGIN', 334);
            $this->cmd(base64_encode($this->user),     334);
            $this->cmd(base64_encode($this->password), 235);

            $this->cmd("MAIL FROM:<{$this->fromEmail}>", 250);
            $this->cmd("RCPT TO:<{$toEmail}>",           250);
            $this->cmd('DATA',                           354);

            fwrite($this->socket, $this->buildMessage($toEmail, $toName, $subject, $html) . "\r\n.\r\n");
            $this->readResponse(250);

            $this->cmd('QUIT', 221);
        } finally {
            $this->disconnect();
        }
    }

    // ── Private helpers ───────────────────────────────────────────────────────

    private function connect(): void
    {
        if ($this->encryption === 'ssl') {
            // SSL context with peer verification
            $ctxOpts = $this->verifyTls
                ? ['ssl' => ['verify_peer' => true, 'verify_peer_name' => true, 'peer_name' => $this->host]]
                : ['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]];
            $ctx     = stream_context_create($ctxOpts);
            $socket  = stream_socket_client(
                "ssl://{$this->host}:{$this->port}", $errno, $errstr, 30,
                STREAM_CLIENT_CONNECT, $ctx
            );
        } else {
            $socket = fsockopen($this->host, $this->port, $errno, $errstr, 30);
        }

        if (!$socket) {
            throw new RuntimeException("SMTP connect failed [{$this->host}:{$this->port}]: {$errstr} ({$errno})");
        }

        $this->socket = $socket;
        stream_set_timeout($this->socket, 30);
        $this->readResponse(220);
    }

    private function disconnect(): void
    {
        if ($this->socket !== null) {
            @fclose($this->socket);
            $this->socket = null;
        }
    }

    /**
     * Write one SMTP command and assert the expected reply code.
     */
    private function cmd(string $command, int $expected): string
    {
        fwrite($this->socket, $command . "\r\n");
        return $this->readResponse($expected);
    }

    /**
     * Read lines until a line with a space after the code (RFC 5321 multi-line).
     */
    private function readResponse(int $expected): string
    {
        $response = '';
        while (true) {
            $line = fgets($this->socket, 512);
            if ($line === false) {
                throw new RuntimeException("SMTP read error (expected {$expected})");
            }
            $response .= $line;
            if (strlen($line) >= 4 && $line[3] === ' ') {
                break;
            }
        }

        $code = (int)substr($response, 0, 3);
        if ($code !== $expected) {
            throw new RuntimeException(
                "SMTP error: expected {$expected}, got {$code}. Response: " . trim($response)
            );
        }

        return $response;
    }

    /**
     * Build a MIME multipart/alternative message (plain-text + HTML).
     */
    private function buildMessage(string $toEmail, string $toName, string $subject, string $html): string
    {
        $boundary = '=_' . md5(uniqid((string)mt_rand(), true));

        $from = $this->mimeAddress($this->fromName, $this->fromEmail);
        $to   = $this->mimeAddress($toName, $toEmail);
        $subj = '=?UTF-8?B?' . base64_encode($subject) . '?=';
        $date = date('r');

        // Plain-text fallback: convert common block elements then strip tags
        $plain = $html;
        $plain = preg_replace('/<br\s*\/?>/i',              "\n",    $plain);
        $plain = preg_replace('/<\/(?:p|div|li|tr|h[1-6])>/i', "\n", $plain);
        $plain = preg_replace('/<[^>]+>/',                  '',      $plain);
        $plain = html_entity_decode($plain, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $plain = preg_replace('/\n{3,}/', "\n\n", trim($plain));

        $lines   = [];
        $lines[] = "From: {$from}";
        $lines[] = "To: {$to}";
        $lines[] = "Subject: {$subj}";
        $lines[] = 'MIME-Version: 1.0';
        $lines[] = "Content-Type: multipart/alternative; boundary=\"{$boundary}\"";
        $lines[] = "Date: {$date}";
        $lines[] = '';
        $lines[] = "--{$boundary}";
        $lines[] = 'Content-Type: text/plain; charset=UTF-8';
        $lines[] = 'Content-Transfer-Encoding: base64';
        $lines[] = '';
        $lines[] = chunk_split(base64_encode(trim($plain)));
        $lines[] = "--{$boundary}";
        $lines[] = 'Content-Type: text/html; charset=UTF-8';
        $lines[] = 'Content-Transfer-Encoding: base64';
        $lines[] = '';
        $lines[] = chunk_split(base64_encode($html));
        $lines[] = "--{$boundary}--";

        return implode("\r\n", $lines);
    }

    /**
     * Format "Display Name <email>" — RFC 2047 encoded for non-ASCII,
     * RFC 5322 quoted-string for ASCII names with special characters.
     */
    private function mimeAddress(string $name, string $email): string
    {
        $name = trim($name);
        if ($name === '') {
            return $email;
        }
        // Non-ASCII: Base64 encode per RFC 2047
        if (preg_match('/[^\x20-\x7E]/', $name)) {
            $name = '=?UTF-8?B?' . base64_encode($name) . '?=';
        } else {
            // RFC 5322 quoted-string: escape only backslash and double-quote
            $name = '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $name) . '"';
        }
        return "{$name} <{$email}>";
    }
}
