<?php

namespace Sethrey\SysvoteWebsite;

/**
 * Minimal SMTP client with STARTTLS or implicit SSL support and AUTH LOGIN.
 * Works with providers like Hostinger (smtp.hostinger.com:465, implicit SSL).
 *
 * No external dependency (no PHPMailer) — talks raw SMTP over a socket.
 */
class SmtpMailer
{
    private string $host;
    private int $port;
    private string $username;
    private string $password;
    private string $fromEmail;
    private string $fromName;

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

    public function __construct(
        string $host,
        int $port,
        string $username,
        string $password,
        string $fromEmail,
        string $fromName = ''
    ) {
        $this->host = $host;
        $this->port = $port;
        $this->username = $username;
        $this->password = $password;
        $this->fromEmail = $fromEmail;
        $this->fromName = $fromName;
    }

    /**
     * @throws \RuntimeException on any SMTP failure
     */
    public function send(
        string $toEmail,
        string $toName,
        string $subject,
        string $body,
        ?string $replyToEmail = null,
        ?string $replyToName = null
    ): void {
        $this->connect();

        try {
            $this->readResponse(220);
            $this->command("EHLO " . $this->safeHostname(), 250);
            $this->command("AUTH LOGIN", 334);
            $this->command(base64_encode($this->username), 334);
            $this->command(base64_encode($this->password), 235);

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

            $headers = $this->buildHeaders($toEmail, $toName, $subject, $replyToEmail, $replyToName);
            $payload = $headers . "\r\n" . $this->encodeBody($body) . "\r\n.";

            $this->write($payload);
            $this->readResponse(250);

            $this->command("QUIT", 221);
        } finally {
            $this->close();
        }
    }

    private function connect(): void
    {
        // Port 465 = implicit TLS/SSL. Port 587 = plaintext then STARTTLS.
        $transport = ($this->port === 465) ? 'ssl://' : 'tcp://';
        $target = $transport . $this->host . ':' . $this->port;

        $context = stream_context_create([
            'ssl' => [
                'verify_peer' => true,
                'verify_peer_name' => true,
            ],
        ]);

        $socket = @stream_socket_client(
            $target,
            $errno,
            $errstr,
            15,
            STREAM_CLIENT_CONNECT,
            $context
        );

        if ($socket === false) {
            throw new \RuntimeException("Connexion SMTP impossible ({$this->host}:{$this->port}) : {$errstr}");
        }

        stream_set_timeout($socket, 15);
        $this->socket = $socket;

        if ($this->port === 587) {
            $this->readResponse(220);
            $this->command("EHLO " . $this->safeHostname(), 250);
            $this->command("STARTTLS", 220);
            if (!stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
                throw new \RuntimeException("Impossible d'activer TLS (STARTTLS).");
            }
        }
    }

    private function command(string $cmd, int $expectedCode): string
    {
        $this->write($cmd);
        return $this->readResponse($expectedCode);
    }

    private function write(string $data): void
    {
        if (@fwrite($this->socket, $data . "\r\n") === false) {
            throw new \RuntimeException("Échec d'écriture sur le socket SMTP.");
        }
    }

    private function readResponse(int $expectedCode): string
    {
        $response = '';
        while (($line = fgets($this->socket, 515)) !== false) {
            $response .= $line;
            // Multiline responses have a "-" after the code; final line has a space.
            if (isset($line[3]) && $line[3] === ' ') {
                break;
            }
        }

        if ($response === '') {
            throw new \RuntimeException("Pas de réponse du serveur SMTP.");
        }

        $code = (int) substr($response, 0, 3);
        if ($code !== $expectedCode) {
            throw new \RuntimeException("Réponse SMTP inattendue (attendu {$expectedCode}) : {$response}");
        }

        return $response;
    }

    private function buildHeaders(
        string $toEmail,
        string $toName,
        string $subject,
        ?string $replyToEmail,
        ?string $replyToName
    ): string {
        $from = $this->fromName !== ''
            ? $this->encodeHeader($this->fromName) . " <{$this->fromEmail}>"
            : $this->fromEmail;

        $to = $toName !== ''
            ? $this->encodeHeader($toName) . " <{$toEmail}>"
            : $toEmail;

        $lines = [
            "From: {$from}",
            "To: {$to}",
            "Subject: " . $this->encodeHeader($subject),
            "Date: " . date('r'),
            "Message-ID: <" . bin2hex(random_bytes(16)) . "@" . $this->safeHostname() . ">",
            "MIME-Version: 1.0",
            "Content-Type: text/plain; charset=UTF-8",
            "Content-Transfer-Encoding: 8bit",
        ];

        if ($replyToEmail !== null) {
            $replyTo = $replyToName
                ? $this->encodeHeader($replyToName) . " <{$replyToEmail}>"
                : $replyToEmail;
            $lines[] = "Reply-To: {$replyTo}";
        }

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

    private function encodeHeader(string $value): string
    {
        return '=?UTF-8?B?' . base64_encode($value) . '?=';
    }

    private function encodeBody(string $body): string
    {
        // Escape lines starting with a lone "." per SMTP DATA rules.
        $lines = explode("\n", str_replace("\r\n", "\n", $body));
        foreach ($lines as &$line) {
            if (isset($line[0]) && $line[0] === '.') {
                $line = '.' . $line;
            }
        }
        return implode("\r\n", $lines);
    }

    private function safeHostname(): string
    {
        return $_SERVER['SERVER_NAME'] ?? 'localhost';
    }

    private function close(): void
    {
        if (is_resource($this->socket)) {
            fclose($this->socket);
        }
        $this->socket = null;
    }
}
