<?php

declare(strict_types=1);

namespace FecaMailshots\Infrastructure;

use FecaMailshots\Application\SmtpSender;

final class BasicSmtpSender implements SmtpSender
{
    public function send(array $credentials, array $to, array $cc, array $bcc, string $subject, string $htmlBody, ?string $replyTo = null, array $attachments = []): array
    {
        $host = (string) ($credentials['smtp_host'] ?? '');
        $port = (int) ($credentials['smtp_port'] ?? 0);
        $user = (string) ($credentials['smtp_user'] ?? '');
        $pass = (string) ($credentials['smtp_password'] ?? '');
        $from = (string) ($credentials['smtp_from_email'] ?? '');
        $fromName = (string) ($credentials['smtp_from_name'] ?? '');

        if ($host === '' || $port <= 0 || $user === '' || $pass === '' || $from === '') {
            throw new \RuntimeException('Missing SMTP credentials.');
        }

        $transport = !empty($credentials['smtp_require_tls']) ? 'tls://' : '';
        $fp = @stream_socket_client($transport . $host . ':' . $port, $errno, $errstr, 20);
        if (!is_resource($fp)) {
            throw new \RuntimeException('SMTP connect failed: ' . $errstr);
        }

        try {
            $this->expect($fp, [220]);
            $this->cmd($fp, 'EHLO localhost', [250]);
            if (empty($credentials['smtp_require_tls'])) {
                // try opportunistic STARTTLS
                $line = $this->cmd($fp, 'STARTTLS', [220], false);
                if ($line !== null) {
                    if (!stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
                        throw new \RuntimeException('Failed to enable STARTTLS crypto.');
                    }
                    $this->cmd($fp, 'EHLO localhost', [250]);
                }
            }

            $this->cmd($fp, 'AUTH LOGIN', [334]);
            $this->cmd($fp, base64_encode($user), [334]);
            $this->cmd($fp, base64_encode($pass), [235]);
            $this->cmd($fp, 'MAIL FROM:<' . $from . '>', [250]);

            $allRecipients = array_values(array_unique(array_merge($to, $cc, $bcc)));
            foreach ($allRecipients as $recipient) {
                $this->cmd($fp, 'RCPT TO:<' . trim($recipient) . '>', [250, 251]);
            }

            $this->cmd($fp, 'DATA', [354]);

            $raw = $this->buildMime($from, $fromName, $to, $cc, $bcc, $subject, $htmlBody, $replyTo, $attachments);
            fwrite($fp, $raw);
            fwrite($fp, "\r\n.\r\n");
            $this->expect($fp, [250]);
            $this->cmd($fp, 'QUIT', [221], false);

            return ['raw_mime' => $raw];
        } finally {
            fclose($fp);
        }
    }

    /** @param list<string> $to @param list<string> $cc @param list<string> $bcc */
    private function buildMime(string $from, string $fromName, array $to, array $cc, array $bcc, string $subject, string $htmlBody, ?string $replyTo, array $attachments): string
    {
        $headers = [];
        $fromHeader = $fromName !== '' ? sprintf('%s <%s>', $this->encodeHeaderValue($fromName), $from) : $from;
        $headers[] = 'From: ' . $fromHeader;
        $headers[] = 'To: ' . implode(', ', $to);
        if ($cc !== []) {
            $headers[] = 'Cc: ' . implode(', ', $cc);
        }
        if ($bcc !== []) {
            $headers[] = 'Bcc: ' . implode(', ', $bcc);
        }
        if ($replyTo !== null && trim($replyTo) !== '') {
            $headers[] = 'Reply-To: ' . trim($replyTo);
        }
        $headers[] = 'Subject: ' . $this->encodeHeaderValue($subject);
        $headers[] = 'MIME-Version: 1.0';
        if ($attachments === []) {
            $headers[] = 'Content-Type: text/html; charset=UTF-8';
            return implode("\r\n", $headers) . "\r\n\r\n" . $htmlBody;
        }

        $boundary = 'feca_mailshots_' . bin2hex(random_bytes(12));
        $mime = implode("\r\n", array_merge($headers, ['Content-Type: multipart/mixed; boundary="' . $boundary . '"'])) . "\r\n\r\n";
        $mime .= '--' . $boundary . "\r\n";
        $mime .= 'Content-Type: text/html; charset=UTF-8' . "\r\n";
        $mime .= 'Content-Transfer-Encoding: 8bit' . "\r\n\r\n";
        $mime .= $htmlBody . "\r\n";

        foreach ($attachments as $attachment) {
            $filename = trim((string) ($attachment['filename'] ?? 'attachment.bin'));
            $mimeType = trim((string) ($attachment['mime_type'] ?? 'application/octet-stream'));
            $bytes = (string) ($attachment['content_bytes'] ?? '');
            if ($filename === '' || $bytes === '') {
                continue;
            }

            $escapedFilename = addcslashes($filename, '"\\');
            $mime .= '--' . $boundary . "\r\n";
            $mime .= 'Content-Type: ' . $mimeType . '; name="' . $escapedFilename . '"' . "\r\n";
            $mime .= 'Content-Transfer-Encoding: base64' . "\r\n";
            $mime .= 'Content-Disposition: attachment; filename="' . $escapedFilename . '"' . "\r\n\r\n";
            $mime .= $this->encodeBase64Chunked($bytes) . "\r\n";
        }

        $mime .= '--' . $boundary . '--' . "\r\n";
        return $mime;
    }

    private function encodeHeaderValue(string $value): string
    {
        $value = preg_replace('/[\r\n]+/', ' ', $value);
        $value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', is_string($value) ? $value : '');
        $value = trim(preg_replace('/[ \t]+/', ' ', is_string($value) ? $value : '') ?? '');
        if ($value === '') {
            return '';
        }

        $chars = [];
        if (preg_match_all('/./us', $value, $matches) === 1) {
            $chars = $matches[0];
        } else {
            $chars = str_split($value);
        }

        $chunks = [];
        $chunk = '';
        foreach ($chars as $char) {
            if ($chunk !== '' && strlen($chunk . $char) > 45) {
                $chunks[] = $chunk;
                $chunk = '';
            }
            $chunk .= $char;
        }
        if ($chunk !== '') {
            $chunks[] = $chunk;
        }

        $encoded = array_map(static fn(string $chunk): string => '=?UTF-8?B?' . base64_encode($chunk) . '?=', $chunks);
        return implode("\r\n ", $encoded);
    }

    private function encodeBase64Chunked(string $bytes): string
    {
        $stream = fopen('php://temp', 'w+b');
        if (!is_resource($stream)) {
            throw new \RuntimeException('Unable to allocate temp stream for attachment encoding.');
        }

        try {
            if (fwrite($stream, $bytes) === false) {
                throw new \RuntimeException('Failed writing attachment bytes to temp stream.');
            }
            rewind($stream);
            $filter = stream_filter_append(
                $stream,
                'convert.base64-encode',
                STREAM_FILTER_READ,
                ['line-length' => 76, 'line-break-chars' => "\r\n"]
            );
            if ($filter === false) {
                throw new \RuntimeException('Failed to initialize base64 stream filter.');
            }

            $encoded = stream_get_contents($stream);
            if (!is_string($encoded)) {
                throw new \RuntimeException('Failed to read encoded attachment bytes.');
            }
            return rtrim($encoded, "\r\n");
        } finally {
            fclose($stream);
        }
    }

    /** @param list<int> $codes */
    private function cmd($fp, string $cmd, array $codes, bool $throwOnMismatch = true): ?string
    {
        fwrite($fp, $cmd . "\r\n");
        return $this->expect($fp, $codes, $throwOnMismatch);
    }

    /** @param list<int> $codes */
    private function expect($fp, array $codes, bool $throwOnMismatch = true): ?string
    {
        $lastLine = null;
        while (true) {
            $line = fgets($fp, 4096);
            if ($line === false) {
                if ($throwOnMismatch) {
                    throw new \RuntimeException('SMTP read failed.');
                }
                return null;
            }

            $lastLine = $line;
            if (!preg_match('/^(\d{3})([\s-])/', $line, $m)) {
                if ($throwOnMismatch) {
                    throw new \RuntimeException('SMTP malformed response: ' . trim($line));
                }
                return null;
            }

            $code = (int) $m[1];
            if (!in_array($code, $codes, true)) {
                if ($throwOnMismatch) {
                    throw new \RuntimeException('SMTP unexpected response: ' . trim($line));
                }
                return null;
            }

            $continuation = $m[2] === '-';
            if (!$continuation) {
                return $lastLine;
            }
        }
    }
}
