<?php
declare(strict_types=1);

namespace App\Services;

/**
 * Minimal pure-PHP SMTP client with STARTTLS + AUTH LOGIN.
 * Built to work with Gmail (smtp.gmail.com:587) on shared hosting where
 * PHPMailer may not be installed. No external deps.
 *
 * Usage:
 *   (new Smtp($host, $port, $user, $pass))->send($from, $fromName, $to, $subject, $htmlBody, $bcc);
 */
class Smtp
{
    private $sock = null;
    private string $lastError = '';

    public function __construct(
        private string $host,
        private int    $port,
        private string $username,
        private string $password,
        private int    $timeout = 15
    ) {}

    public function lastError(): string { return $this->lastError; }

    /**
     * Send a single email. $to, $bcc may be strings or arrays of addresses.
     * Returns true on success.
     */
    public function send(
        string $fromEmail,
        string $fromName,
        string|array $to,
        string $subject,
        string $htmlBody,
        string|array $bcc = [],
        ?string $replyTo = null,
        array $attachments = []  // [ [ 'name' => 'signed-form.pdf', 'content' => $bytes, 'type' => 'application/pdf' ] ]
    ): bool {
        $toList  = is_array($to)  ? $to  : [$to];
        $bccList = is_array($bcc) ? $bcc : ($bcc ? [$bcc] : []);
        $rcpts = array_merge($toList, $bccList);
        if (empty($rcpts)) { $this->lastError = 'No recipients'; return false; }

        $this->sock = @stream_socket_client("tcp://{$this->host}:{$this->port}", $errno, $errstr, $this->timeout);
        if (!$this->sock) {
            $this->lastError = "Connect failed: {$errstr} ({$errno})";
            return false;
        }
        stream_set_timeout($this->sock, $this->timeout);

        try {
            $this->expect('220');
            $this->cmd("EHLO mysticartatto.local");
            $this->expect('250');

            if ($this->port === 587) {
                $this->cmd("STARTTLS");
                $this->expect('220');
                if (!stream_socket_enable_crypto($this->sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
                    $this->lastError = 'STARTTLS failed';
                    return false;
                }
                $this->cmd("EHLO mysticartatto.local");
                $this->expect('250');
            }

            $this->cmd("AUTH LOGIN");
            $this->expect('334');
            $this->cmd(base64_encode($this->username));
            $this->expect('334');
            $this->cmd(base64_encode($this->password));
            $this->expect('235');

            $this->cmd("MAIL FROM:<{$fromEmail}>");
            $this->expect('250');
            foreach ($rcpts as $addr) {
                $this->cmd("RCPT TO:<{$addr}>");
                $this->expect('250');
            }
            $this->cmd("DATA");
            $this->expect('354');

            // Build MIME message
            $boundary = 'MAT-' . bin2hex(random_bytes(8));
            $altBoundary = 'ALT-' . bin2hex(random_bytes(8));
            $headers  = "From: \"{$fromName}\" <{$fromEmail}>\r\n";
            $headers .= "To: " . implode(', ', array_map(fn($a) => "<{$a}>", $toList)) . "\r\n";
            if ($replyTo) $headers .= "Reply-To: <{$replyTo}>\r\n";
            $headers .= "Subject: " . self::encodeHeader($subject) . "\r\n";
            $headers .= "MIME-Version: 1.0\r\n";
            $headers .= "Date: " . date('r') . "\r\n";
            $headers .= "Message-ID: <" . bin2hex(random_bytes(12)) . "@mysticartatto.local>\r\n";

            if (empty($attachments)) {
                $headers .= "Content-Type: multipart/alternative; boundary=\"{$altBoundary}\"\r\n";
                $body = self::altBody($altBoundary, $htmlBody);
            } else {
                $headers .= "Content-Type: multipart/mixed; boundary=\"{$boundary}\"\r\n";
                $body = "--{$boundary}\r\n";
                $body .= "Content-Type: multipart/alternative; boundary=\"{$altBoundary}\"\r\n\r\n";
                $body .= self::altBody($altBoundary, $htmlBody);
                foreach ($attachments as $att) {
                    $body .= "\r\n--{$boundary}\r\n";
                    $body .= "Content-Type: " . ($att['type'] ?? 'application/octet-stream') . "; name=\"" . $att['name'] . "\"\r\n";
                    $body .= "Content-Transfer-Encoding: base64\r\n";
                    $body .= "Content-Disposition: attachment; filename=\"" . $att['name'] . "\"\r\n\r\n";
                    $body .= chunk_split(base64_encode($att['content']));
                }
                $body .= "--{$boundary}--\r\n";
            }

            // Dot-stuff: lines starting with "." must be escaped to ".."
            $payload = $headers . "\r\n" . $body;
            $payload = preg_replace('/(^|\r\n)\./', '$1..', $payload);
            fwrite($this->sock, $payload . "\r\n.\r\n");
            $this->expect('250');

            $this->cmd("QUIT");
            return true;
        } catch (\Throwable $e) {
            $this->lastError = $e->getMessage();
            return false;
        } finally {
            if (is_resource($this->sock)) @fclose($this->sock);
        }
    }

    private static function altBody(string $altBoundary, string $htmlBody): string
    {
        $textBody = trim(strip_tags($htmlBody));
        $body  = "--{$altBoundary}\r\n";
        $body .= "Content-Type: text/plain; charset=UTF-8\r\n";
        $body .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
        $body .= $textBody . "\r\n\r\n";
        $body .= "--{$altBoundary}\r\n";
        $body .= "Content-Type: text/html; charset=UTF-8\r\n";
        $body .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
        $body .= $htmlBody . "\r\n\r\n";
        $body .= "--{$altBoundary}--\r\n";
        return $body;
    }

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

    private function cmd(string $line): void
    {
        fwrite($this->sock, $line . "\r\n");
    }

    private function expect(string $code): void
    {
        $response = '';
        while (!feof($this->sock)) {
            $line = fgets($this->sock, 8192);
            if ($line === false) break;
            $response .= $line;
            // Continuation: "250-..." then terminator "250 ..."
            if (isset($line[3]) && $line[3] === ' ') break;
        }
        if (strncmp($response, $code, strlen($code)) !== 0) {
            throw new \RuntimeException("SMTP expected {$code}, got: " . trim($response));
        }
    }
}
