<?php

declare(strict_types=1);

namespace App\Framework\Mail;

use App\Framework\Mail\Exceptions\SmtpException;

final class SmtpTransport implements TransportInterface
{
    private $socket = null;

    private bool $connected = false;

    public function __construct(
        private readonly SmtpConfig $config,
    ) {
    }

    public function send(Message $message): TransportResult
    {
        try {
            $this->connect();
            $this->authenticate();

            $messageId = $this->sendMessage($message);

            return TransportResult::success($messageId);

        } catch (SmtpException $e) {
            return TransportResult::failure(
                error: $e->getMessage(),
                metadata: $e->getContext()->toArray()
            );
        } catch (\Exception $e) {
            return TransportResult::failure($e->getMessage());
        } finally {
            $this->disconnect();
        }
    }

    public function isAvailable(): bool
    {
        try {
            $this->connect();
            $this->disconnect();

            return true;
        } catch (\Exception) {
            return false;
        }
    }

    public function getName(): string
    {
        return 'SMTP Transport (' . $this->config->host . ':' . $this->config->port . ')';
    }

    private function connect(): void
    {
        if ($this->connected) {
            return;
        }

        $context = stream_context_create([
            'ssl' => [
                'verify_peer' => $this->config->verifyPeer,
                'verify_peer_name' => $this->config->verifyPeer,
            ],
        ]);

        $connectionString = $this->config->getConnectionString();

        $this->socket = stream_socket_client(
            $connectionString,
            $errno,
            $errstr,
            $this->config->timeout,
            STREAM_CLIENT_CONNECT,
            $context
        );

        if (! $this->socket) {
            throw SmtpException::connectionFailed($this->config->host, $this->config->port, "$errstr ($errno)");
        }

        stream_set_timeout($this->socket, $this->config->timeout);

        // Read greeting
        $response = $this->readResponse();
        if (! str_starts_with($response, '220')) {
            throw SmtpException::commandFailed('GREETING', $response);
        }

        // Send EHLO
        $this->sendCommand('EHLO ' . gethostname());
        $response = $this->readResponse();
        if (! str_starts_with($response, '250')) {
            throw SmtpException::commandFailed('EHLO', $response);
        }

        // Start TLS if needed
        if ($this->config->encryption === 'tls') {
            $this->sendCommand('STARTTLS');
            $response = $this->readResponse();
            if (! str_starts_with($response, '220')) {
                throw SmtpException::commandFailed('STARTTLS', $response);
            }

            if (! stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
                throw SmtpException::tlsFailed();
            }

            // Send EHLO again after TLS
            $this->sendCommand('EHLO ' . gethostname());
            $response = $this->readResponse();
            if (! str_starts_with($response, '250')) {
                throw SmtpException::commandFailed('EHLO_AFTER_TLS', $response);
            }
        }

        $this->connected = true;
    }

    private function authenticate(): void
    {
        if (! $this->config->requiresAuthentication()) {
            return;
        }

        $this->sendCommand('AUTH LOGIN');
        $response = $this->readResponse();
        if (! str_starts_with($response, '334')) {
            throw SmtpException::authenticationFailed($response);
        }

        $this->sendCommand(base64_encode($this->config->username));
        $response = $this->readResponse();
        if (! str_starts_with($response, '334')) {
            throw SmtpException::authenticationFailed($response);
        }

        $this->sendCommand(base64_encode($this->config->password));
        $response = $this->readResponse();
        if (! str_starts_with($response, '235')) {
            throw SmtpException::authenticationFailed($response);
        }
    }

    private function sendMessage(Message $message): string
    {
        // MAIL FROM
        $this->sendCommand('MAIL FROM:<' . $message->from->value . '>');
        $response = $this->readResponse();
        if (! str_starts_with($response, '250')) {
            throw SmtpException::mailFromFailed($message->from->value, $response);
        }

        // RCPT TO
        foreach ($message->getAllRecipients() as $recipient) {
            $this->sendCommand('RCPT TO:<' . $recipient->value . '>');
            $response = $this->readResponse();
            if (! str_starts_with($response, '250')) {
                throw SmtpException::recipientFailed($recipient->value, $response);
            }
        }

        // DATA
        $this->sendCommand('DATA');
        $response = $this->readResponse();
        if (! str_starts_with($response, '354')) {
            throw SmtpException::dataCommandFailed($response);
        }

        // Send message content
        $messageContent = $this->buildMessage($message);
        $this->sendCommand($messageContent);
        $this->sendCommand('.');

        $response = $this->readResponse();
        if (! str_starts_with($response, '250')) {
            throw SmtpException::messageSendFailed($response);
        }

        // Extract message ID from response
        $messageId = $this->extractMessageId($response);

        return $messageId;
    }

    private function buildMessage(Message $message): string
    {
        $lines = [];

        // Headers
        $lines[] = 'From: ' . $this->encodeHeader($message->from->value);
        $lines[] = 'To: ' . $this->encodeHeader($message->to->toString());

        if ($message->cc->isNotEmpty()) {
            $lines[] = 'Cc: ' . $this->encodeHeader($message->cc->toString());
        }

        if ($message->replyTo !== null) {
            $lines[] = 'Reply-To: ' . $this->encodeHeader($message->replyTo->value);
        }

        $lines[] = 'Subject: ' . $this->encodeHeader($message->subject);
        $lines[] = 'Date: ' . date('r');
        $lines[] = 'Message-ID: <' . $this->generateMessageId() . '>';
        $lines[] = 'X-Mailer: Custom PHP SMTP Transport';

        // Priority
        if ($message->priority !== Priority::NORMAL) {
            $lines[] = 'X-Priority: ' . $message->priority->value;
            $lines[] = 'Importance: ' . $message->priority->toImportanceValue();
        }

        // Custom headers
        foreach ($message->headers as $name => $value) {
            $lines[] = $this->sanitizeHeaderName($name) . ': ' . $this->encodeHeader($value);
        }

        // MIME structure
        if ($message->hasHtmlBody() && ! empty($message->body)) {
            // Both HTML and text - use multipart/alternative
            return $this->buildMultipartAlternativeMessage($message, $lines);
        } elseif ($message->hasAttachments()) {
            // Has attachments - use multipart/mixed
            return $this->buildMultipartMixedMessage($message, $lines);
        } elseif ($message->hasHtmlBody()) {
            // HTML only
            return $this->buildHtmlMessage($message, $lines);
        } else {
            // Plain text only
            return $this->buildTextMessage($message, $lines);
        }
    }

    private function buildTextMessage(Message $message, array $lines): string
    {
        $lines[] = 'MIME-Version: 1.0';
        $lines[] = 'Content-Type: text/plain; charset=UTF-8';
        $lines[] = 'Content-Transfer-Encoding: 8bit';
        $lines[] = '';
        $lines[] = $message->body;

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

    private function buildHtmlMessage(Message $message, array $lines): string
    {
        $lines[] = 'MIME-Version: 1.0';
        $lines[] = 'Content-Type: text/html; charset=UTF-8';
        $lines[] = 'Content-Transfer-Encoding: 8bit';
        $lines[] = '';
        $lines[] = $message->htmlBody;

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

    private function buildMultipartAlternativeMessage(Message $message, array $lines): string
    {
        $generator = new \App\Framework\Ulid\UlidGenerator();
        $boundary = 'alt_' . $generator->generate();

        $lines[] = 'MIME-Version: 1.0';

        if ($message->hasAttachments()) {
            // Mixed with alternative inside
            $mixedBoundary = 'mixed_' . $generator->generate();
            $lines[] = 'Content-Type: multipart/mixed; boundary="' . $mixedBoundary . '"';
            $lines[] = '';
            $lines[] = '--' . $mixedBoundary;
            $lines[] = 'Content-Type: multipart/alternative; boundary="' . $boundary . '"';
            $lines[] = '';

            $this->addAlternativeParts($lines, $message, $boundary);

            $this->addAttachments($lines, $message, $mixedBoundary);
            $lines[] = '--' . $mixedBoundary . '--';
        } else {
            // Just alternative
            $lines[] = 'Content-Type: multipart/alternative; boundary="' . $boundary . '"';
            $lines[] = '';

            $this->addAlternativeParts($lines, $message, $boundary);
            $lines[] = '--' . $boundary . '--';
        }

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

    private function buildMultipartMixedMessage(Message $message, array $lines): string
    {
        $generator = new \App\Framework\Ulid\UlidGenerator();
        $boundary = 'mixed_' . $generator->generate();

        $lines[] = 'MIME-Version: 1.0';
        $lines[] = 'Content-Type: multipart/mixed; boundary="' . $boundary . '"';
        $lines[] = '';

        // Add main content
        $lines[] = '--' . $boundary;
        if ($message->hasHtmlBody()) {
            $lines[] = 'Content-Type: text/html; charset=UTF-8';
            $lines[] = 'Content-Transfer-Encoding: 8bit';
            $lines[] = '';
            $lines[] = $message->htmlBody;
        } else {
            $lines[] = 'Content-Type: text/plain; charset=UTF-8';
            $lines[] = 'Content-Transfer-Encoding: 8bit';
            $lines[] = '';
            $lines[] = $message->body;
        }

        $this->addAttachments($lines, $message, $boundary);
        $lines[] = '--' . $boundary . '--';

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

    private function addAlternativeParts(array &$lines, Message $message, string $boundary): void
    {
        // Text part
        $lines[] = '--' . $boundary;
        $lines[] = 'Content-Type: text/plain; charset=UTF-8';
        $lines[] = 'Content-Transfer-Encoding: 8bit';
        $lines[] = '';
        $lines[] = $message->body;
        $lines[] = '';

        // HTML part
        $lines[] = '--' . $boundary;
        $lines[] = 'Content-Type: text/html; charset=UTF-8';
        $lines[] = 'Content-Transfer-Encoding: 8bit';
        $lines[] = '';
        $lines[] = $message->htmlBody;
        $lines[] = '';
        $lines[] = '--' . $boundary . '--';
    }

    private function addAttachments(array &$lines, Message $message, string $boundary): void
    {
        foreach ($message->attachments as $attachment) {
            $lines[] = '';
            $lines[] = '--' . $boundary;
            $lines[] = 'Content-Type: ' . $attachment->mimeType->value . '; name="' . $this->encodeHeader($attachment->name) . '"';
            $lines[] = 'Content-Transfer-Encoding: base64';

            if ($attachment->isInline && $attachment->contentId) {
                $lines[] = 'Content-Disposition: inline; filename="' . $this->encodeHeader($attachment->name) . '"';
                $lines[] = 'Content-ID: <' . $attachment->contentId . '>';
            } else {
                $lines[] = 'Content-Disposition: attachment; filename="' . $this->encodeHeader($attachment->name) . '"';
            }

            $lines[] = '';
            $lines[] = chunk_split($attachment->getBase64Content(), 76, "\r\n");
        }
    }

    private function encodeHeader(string $value): string
    {
        // RFC 2047 header encoding for non-ASCII characters
        if (mb_check_encoding($value, 'ASCII')) {
            return $value;
        }

        return '=?UTF-8?B?' . base64_encode($value) . '?=';
    }

    private function sanitizeHeaderName(string $name): string
    {
        // Remove any potentially dangerous characters from header names
        return preg_replace('/[^\w-]/', '', $name);
    }

    private function generateMessageId(): string
    {
        $generator = new \App\Framework\Ulid\UlidGenerator();
        return $generator->generate() . '.' . time() . '@' . gethostname();
    }

    private function sendCommand(string $command): void
    {
        if (! $this->socket) {
            throw SmtpException::socketError($this->config->host, $this->config->port, 'sending command');
        }

        fwrite($this->socket, $command . "\r\n");
    }

    private function readResponse(): string
    {
        if (! $this->socket) {
            throw SmtpException::socketError($this->config->host, $this->config->port, 'reading response');
        }

        $response = '';
        while (($line = fgets($this->socket, 512)) !== false) {
            $response .= $line;
            if (substr($line, 3, 1) === ' ') {
                break;
            }
        }

        return trim($response);
    }

    private function extractMessageId(string $response): string
    {
        // Try to extract message ID from SMTP response
        if (preg_match('/id=([a-zA-Z0-9\-._]+)/', $response, $matches)) {
            return $matches[1];
        }

        // Fallback to generated ID
        $generator = new \App\Framework\Ulid\UlidGenerator();
        return $generator->generate() . '@' . gethostname();
    }

    private function disconnect(): void
    {
        if (! $this->connected || ! $this->socket) {
            return;
        }

        try {
            $this->sendCommand('QUIT');
            $this->readResponse();
        } catch (\Exception) {
            // Ignore errors during disconnect
        }

        fclose($this->socket);
        $this->socket = null;
        $this->connected = false;
    }
}
