<?php

class SimpleSmtpMailer
{
    private string $host;
    private int $port;
    private string $username;
    private string $password;
    private string $encryption;
    private $socket = null;
    private string $lastResponse = '';
    private array $capabilities = [];

    public function __construct(string $host, int $port, string $username, string $password, string $encryption = 'tls')
    {
        $this->host = trim($host);
        $this->port = (int)$port;
        $this->username = trim($username);
        $this->password = $password;
        $this->encryption = strtolower(trim($encryption));
    }

    public function send(string $fromEmail, string $fromName, string $toEmail, string $subject, string $body): void
    {
        $this->connect();

        try {
            $this->expect([220], 'connect');

            $this->ehlo();

            if ($this->encryption === 'tls') {
                $this->command('STARTTLS', [220], 'STARTTLS');

                $cryptoEnabled = @stream_socket_enable_crypto(
                    $this->socket,
                    true,
                    STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT
                );

                if ($cryptoEnabled !== true) {
                    throw new RuntimeException('SMTP ошибка на этапе STARTTLS: не удалось включить TLS.');
                }

                $this->ehlo();
            }

            if ($this->username !== '') {
                $this->authenticate();
            }

            $this->command('MAIL FROM:<' . $fromEmail . '>', [250], 'MAIL FROM');
            $this->command('RCPT TO:<' . $toEmail . '>', [250, 251], 'RCPT TO');
            $this->command('DATA', [354], 'DATA');

            $headers = [
                'Date: ' . date(DATE_RFC2822),
                'From: ' . $this->encodeHeader($fromName) . ' <' . $fromEmail . '>',
                'To: <' . $toEmail . '>',
                'Subject: ' . $this->encodeHeader($subject),
                'MIME-Version: 1.0',
                'Content-Type: text/plain; charset=UTF-8',
                'Content-Transfer-Encoding: 8bit',
            ];

            $message =
                implode("\r\n", $headers) .
                "\r\n\r\n" .
                $this->normalizeBody($body) .
                "\r\n.\r\n";

            $this->write($message, 'message body');
            $this->expect([250], 'message body');

            $this->command('QUIT', [221], 'QUIT');
        } finally {
            if (is_resource($this->socket)) {
                fclose($this->socket);
            }
            $this->socket = null;
        }
    }

    private function connect(): void
    {
        $remoteHost = $this->host;
        $contextOptions = [];

        if ($this->encryption === 'ssl') {
            $remoteHost = 'ssl://' . $this->host;
            $contextOptions['ssl'] = [
                'verify_peer' => true,
                'verify_peer_name' => true,
                'allow_self_signed' => false,
                'peer_name' => $this->host,
                'SNI_enabled' => true,
            ];
        }

        $context = stream_context_create($contextOptions);

        $this->socket = @stream_socket_client(
            $remoteHost . ':' . $this->port,
            $errno,
            $errstr,
            20,
            STREAM_CLIENT_CONNECT,
            $context
        );

        if (!$this->socket) {
            throw new RuntimeException('Ошибка соединения с SMTP: ' . $errstr . ' (' . $errno . ')');
        }

        stream_set_timeout($this->socket, 20);
    }

    private function ehlo(): void
    {
        $this->write("EHLO localhost\r\n", 'EHLO');
        $response = $this->readResponse('EHLO');

        $code = (int)substr($response, 0, 3);
        if ($code !== 250) {
            throw new RuntimeException('SMTP ошибка на этапе EHLO: ' . trim($response));
        }

        $this->capabilities = $this->parseCapabilities($response);
    }

    private function authenticate(): void
    {
        $authLine = $this->capabilities['AUTH'] ?? '';
        $authLineUpper = strtoupper($authLine);

        if (strpos($authLineUpper, 'LOGIN') !== false) {
            $this->command('AUTH LOGIN', [334], 'AUTH LOGIN');
            $this->command(base64_encode($this->username), [334], 'AUTH LOGIN username');
            $this->command(base64_encode($this->password), [235], 'AUTH LOGIN password');
            return;
        }

        if (strpos($authLineUpper, 'PLAIN') !== false) {
            $plain = base64_encode("\0" . $this->username . "\0" . $this->password);
            $this->command('AUTH PLAIN ' . $plain, [235], 'AUTH PLAIN');
            return;
        }

        if ($authLine !== '') {
            throw new RuntimeException('SMTP сервер не поддерживает LOGIN/PLAIN. AUTH: ' . $authLine);
        }

        throw new RuntimeException('SMTP сервер не прислал возможности AUTH после EHLO.');
    }

    private function parseCapabilities(string $response): array
    {
        $caps = [];
        $lines = preg_split("/\r\n|\n|\r/", trim($response));

        foreach ($lines as $line) {
            if (preg_match('/^250[\-\s](.+)$/i', $line, $m)) {
                $value = trim($m[1]);
                $parts = preg_split('/\s+/', $value, 2);
                $key = strtoupper($parts[0]);
                $rest = $parts[1] ?? '';
                $caps[$key] = $rest;
            }
        }

        return $caps;
    }

    private function command(string $command, array $expectedCodes, string $stage): void
    {
        $this->write($command . "\r\n", $stage);
        $this->expect($expectedCodes, $stage);
    }

    private function write(string $data, string $stage): void
    {
        $result = fwrite($this->socket, $data);

        if ($result === false) {
            throw new RuntimeException('Не удалось отправить данные в SMTP на этапе ' . $stage . '.');
        }
    }

    private function expect(array $expectedCodes, string $stage): void
    {
        $response = $this->readResponse($stage);
        $code = (int)substr($response, 0, 3);

        if (!in_array($code, $expectedCodes, true)) {
            throw new RuntimeException('SMTP ошибка на этапе ' . $stage . ': ' . trim($response));
        }
    }

    private function readResponse(string $stage): string
    {
        $response = '';

        while (($line = fgets($this->socket, 515)) !== false) {
            $response .= $line;
            if (isset($line[3]) && $line[3] === ' ') {
                break;
            }
        }

        if ($response === '') {
            throw new RuntimeException('SMTP сервер не ответил на этапе ' . $stage . '.');
        }

        $this->lastResponse = $response;
        return $response;
    }

    private function encodeHeader(string $value): string
    {
        return '=?UTF-8?B?' . base64_encode($value) . '?=';
    }

    private function normalizeBody(string $body): string
    {
        $body = str_replace(["\r\n", "\r"], "\n", $body);
        $lines = explode("\n", $body);

        foreach ($lines as &$line) {
            if (strncmp($line, '.', 1) === 0) {
                $line = '.' . $line;
            }
        }

        return implode("\r\n", $lines);
    }
}