<?php
declare(strict_types=1);

namespace Celena\Core\Mail;

use Celena\Core\Logger;
use Celena\Module\Settings\Options;
use RuntimeException;

/**
 * Простой SMTP-клиент без внешних зависимостей.
 * Для «log»-драйвера складывает письма в storage/logs/mail-*.log (удобно в разработке).
 * Для «smtp» — базовый AUTH LOGIN, STARTTLS при наличии.
 *
 * Настройки через Options:
 *   mail_driver       smtp | log | mail   (default: log)
 *   mail_from         info@example.com
 *   mail_from_name    Celena
 *   mail_smtp_host    smtp.gmail.com
 *   mail_smtp_port    587
 *   mail_smtp_user    user@example.com
 *   mail_smtp_pass    password
 *   mail_smtp_secure  tls | ssl | ""
 */
final class Mailer
{
    public function __construct(
        private readonly Options $options,
        private readonly Logger $logger,
        private readonly string $logDir,
    ) {
    }

    /**
     * @param array<int,array{filename:string,content:string,mime?:string}> $attachments
     */
    public function send(string $to, string $subject, string $html, ?string $text = null, array $attachments = []): bool
    {
        $driver = (string) $this->options->get('mail_driver', 'log');
        $from = (string) $this->options->get('mail_from', 'no-reply@localhost');
        $fromName = (string) $this->options->get('mail_from_name', 'Celena');
        $text = $text ?? trim(strip_tags((string) preg_replace('/<br\s*\/?>/i', "\n", $html)));

        return match ($driver) {
            'smtp' => $this->sendSmtp($to, $subject, $html, $text, $from, $fromName, $attachments),
            'mail' => $this->sendPhpMail($to, $subject, $html, $text, $from, $fromName, $attachments),
            default => $this->sendLog($to, $subject, $html, $from, $fromName, $attachments),
        };
    }

    private function sendLog(string $to, string $subject, string $html, string $from, string $fromName, array $attachments = []): bool
    {
        if (!is_dir($this->logDir)) {
            @mkdir($this->logDir, 0755, true);
        }
        $att = $attachments !== []
            ? "\nAttachments: " . implode(', ', array_map(static fn ($a) => (string) ($a['filename'] ?? 'file'), $attachments))
            : '';
        $file = $this->logDir . '/mail-' . date('Y-m-d') . '.log';
        $entry = sprintf(
            "=== [%s] %s → %s\nFrom: %s <%s>\nSubject: %s%s\n\n%s\n\n",
            date('H:i:s'), $from, $to, $fromName, $from, $subject, $att, $html
        );
        @file_put_contents($file, $entry, FILE_APPEND | LOCK_EX);
        $this->logger->info("Mail logged", ['to' => $to, 'subject' => $subject]);
        return true;
    }

    private function sendPhpMail(string $to, string $subject, string $html, string $text, string $from, string $fromName, array $attachments = []): bool
    {
        $mime = $this->composeMime($html, $text, $attachments);
        $headers = [
            'MIME-Version: 1.0',
            rtrim($mime['headers']),
            'From: ' . $this->encodeHeader($fromName) . ' <' . $from . '>',
        ];
        $ok = @mail($to, $this->encodeHeader($subject), $mime['body'], implode("\r\n", $headers));
        if (!$ok) {
            $this->logger->error('mail() failed', ['to' => $to]);
        }
        return (bool) $ok;
    }

    /**
     * Собирает MIME-тело письма: multipart/alternative (text+html), а при наличии
     * вложений — оборачивает в multipart/mixed.
     * @return array{headers:string, body:string}
     */
    private function composeMime(string $html, string $text, array $attachments): array
    {
        $alt = 'alt_' . bin2hex(random_bytes(8));
        $altPart = '--' . $alt . "\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n" . $text . "\r\n"
            . '--' . $alt . "\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n" . $html . "\r\n"
            . '--' . $alt . "--\r\n";

        if ($attachments === []) {
            return ['headers' => 'Content-Type: multipart/alternative; boundary="' . $alt . '"' . "\r\n", 'body' => $altPart];
        }

        $mix = 'mix_' . bin2hex(random_bytes(8));
        $body = '--' . $mix . "\r\nContent-Type: multipart/alternative; boundary=\"" . $alt . "\"\r\n\r\n" . $altPart;
        foreach ($attachments as $a) {
            $fn = preg_replace('/[^\w.\- ]/', '_', (string) ($a['filename'] ?? 'file'));
            $mimeType = (string) ($a['mime'] ?? 'application/octet-stream');
            $data = chunk_split(base64_encode((string) ($a['content'] ?? '')));
            $body .= '--' . $mix . "\r\nContent-Type: " . $mimeType . '; name="' . $fn . "\"\r\n"
                . "Content-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=\"" . $fn . "\"\r\n\r\n"
                . $data . "\r\n";
        }
        $body .= '--' . $mix . "--\r\n";
        return ['headers' => 'Content-Type: multipart/mixed; boundary="' . $mix . '"' . "\r\n", 'body' => $body];
    }

    private function sendSmtp(string $to, string $subject, string $html, string $text, string $from, string $fromName, array $attachments = []): bool
    {
        $host = (string) $this->options->get('mail_smtp_host', '');
        $port = (int) ($this->options->get('mail_smtp_port') ?? 587);
        $user = (string) $this->options->get('mail_smtp_user', '');
        $pass = (string) $this->options->get('mail_smtp_pass', '');
        $secure = (string) $this->options->get('mail_smtp_secure', 'tls');
        if ($host === '') {
            throw new RuntimeException('SMTP host не настроен.');
        }

        $connStr = ($secure === 'ssl' ? 'ssl://' : '') . $host . ':' . $port;
        $fp = @stream_socket_client($connStr, $errno, $errstr, 30);
        if ($fp === false) {
            throw new RuntimeException('SMTP connect failed: ' . $errstr);
        }
        stream_set_timeout($fp, 30);

        $read = function () use ($fp): string {
            $out = '';
            while (!feof($fp)) {
                $line = fgets($fp, 515);
                if ($line === false) break;
                $out .= $line;
                if (isset($line[3]) && $line[3] === ' ') break;
            }
            return $out;
        };
        $send = function (string $cmd) use ($fp, $read): string {
            fwrite($fp, $cmd . "\r\n");
            return $read();
        };

        $read(); // greeting
        $send('EHLO ' . gethostname());

        if ($secure === 'tls') {
            $send('STARTTLS');
            if (!stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
                fclose($fp);
                throw new RuntimeException('SMTP STARTTLS failed.');
            }
            $send('EHLO ' . gethostname());
        }

        if ($user !== '' && $pass !== '') {
            $auth = $send('AUTH LOGIN');
            if (!str_starts_with($auth, '334')) {
                fclose($fp);
                throw new RuntimeException('SMTP AUTH not accepted.');
            }
            $send(base64_encode($user));
            $resp = $send(base64_encode($pass));
            if (!str_starts_with($resp, '235')) {
                fclose($fp);
                throw new RuntimeException('SMTP auth failed: ' . trim($resp));
            }
        }

        $send('MAIL FROM: <' . $from . '>');
        $send('RCPT TO: <' . $to . '>');
        $dataResp = $send('DATA');
        if (!str_starts_with($dataResp, '354')) {
            fclose($fp);
            throw new RuntimeException('SMTP DATA rejected: ' . trim($dataResp));
        }

        $mime = $this->composeMime($html, $text, $attachments);
        $headers  = 'From: ' . $this->encodeHeader($fromName) . ' <' . $from . '>' . "\r\n";
        $headers .= 'To: <' . $to . '>' . "\r\n";
        $headers .= 'Subject: ' . $this->encodeHeader($subject) . "\r\n";
        $headers .= "MIME-Version: 1.0\r\n";
        $headers .= $mime['headers'] . "\r\n";
        fwrite($fp, $headers . $mime['body'] . "\r\n.\r\n");
        $ok = str_starts_with($read(), '250');
        $send('QUIT');
        fclose($fp);

        if (!$ok) {
            $this->logger->error('SMTP send failed', ['to' => $to]);
        }
        return $ok;
    }

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