<?php

namespace App\Services\Email;

use App\Mail\DynamicEmail;
use App\Models\EmailAccount;
use App\Models\Message;
use App\Models\UsageRecord;
use App\Support\SmtpConfig;
use Google\Client as GoogleClient;
use Google\Service\Gmail;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;

class EmailSendService
{
    /**
     * Send a reply message using the appropriate provider.
     */
    public function sendReply(Message $replyMessage, EmailAccount $account): bool
    {
        try {
            // Inject tracking if enabled
            $bodyHtml = $this->injectTracking($replyMessage);

            // Knowledge Agent — append the "Was this helpful?" thumbs links
            // to AI-authored replies that have a signed feedback token.
            // Runs after injectTracking so the click-tracker doesn't wrap
            // the thumbs URLs (we want raw kb/feedback links, not
            // rewritten /api/track/click/... URLs that would consume the
            // token in a 302 redirect).
            $bodyHtml = $this->injectKbFeedback($replyMessage, $bodyHtml);

            $result = match ($account->provider) {
                'imap', 'custom' => $this->sendViaSMTP($replyMessage, $account, $bodyHtml),
                'gmail' => $this->sendViaGmail($replyMessage, $account, $bodyHtml),
                'outlook' => $this->sendViaOutlook($replyMessage, $account, $bodyHtml),
                default => throw new \InvalidArgumentException("Unsupported provider: {$account->provider}"),
            };

            if ($result) {
                $replyMessage->update([
                    'delivery_status' => 'sent',
                    'sent_at' => now(),
                ]);

                // Track email send usage for plan limit enforcement. Key MUST
                // be 'emails_per_month' — matches PlanLimitService::canUse(),
                // the admin Resource Limits UI, and the plan_features cap.
                if ($replyMessage->workspace_id) {
                    UsageRecord::incrementUsage($replyMessage->workspace_id, 'emails_per_month');
                }

                Log::info("EmailSend: Message {$replyMessage->id} sent via {$account->provider}");
            }

            return $result;
        } catch (\Throwable $e) {
            Log::error("EmailSend: Failed to send message {$replyMessage->id}: {$e->getMessage()}", [
                'message_id' => $replyMessage->id,
                'account_id' => $account->id,
                'provider' => $account->provider,
            ]);

            $replyMessage->update([
                'delivery_status' => 'failed',
                'delivery_error' => Str::limit($e->getMessage(), 500),
            ]);

            throw $e;
        }
    }

    /**
     * Send via SMTP using Laravel Mail with a dynamically configured mailer.
     */
    protected function sendViaSMTP(Message $replyMessage, EmailAccount $account, string $bodyHtml): bool
    {
        $mailerName = 'dynamic_smtp_' . $account->id;
        $mailerKey = "mail.mailers.{$mailerName}";
        $timeout = $account->smtp_timeout ?? config('mail.timeout', 30);

        // Save original config so we can restore it (not just null it out)
        $originalConfig = config($mailerKey);

        // Set up dynamic SMTP configuration at runtime
        Config::set($mailerKey, SmtpConfig::build([
            'host' => $account->smtp_host,
            'port' => $account->smtp_port,
            'encryption' => $account->smtp_encryption,
            'username' => $account->smtp_username,
            'password' => $account->smtp_password, // auto-decrypted via model cast
            'timeout' => $timeout,
            'local_domain' => parse_url(config('app.url'), PHP_URL_HOST),
        ]));

        try {
            $signature = $this->getSignature($account, $replyMessage);

            $mailable = new DynamicEmail(
                toAddresses: $replyMessage->to_emails ?? [],
                ccAddresses: $replyMessage->cc_emails ?? [],
                bccAddresses: $replyMessage->bcc_emails ?? [],
                emailSubject: $replyMessage->subject ?? $replyMessage->conversation?->subject ?? '(no subject)',
                bodyHtml: $bodyHtml,
                attachmentPaths: $this->getAttachmentPaths($replyMessage),
                signatureHtml: $signature,
                fromName: $replyMessage->from_name ?? $account->display_name ?? $account->email,
                fromEmail: $replyMessage->from_email ?? $account->email,
                inReplyTo: $replyMessage->in_reply_to,
                referencesHeader: $replyMessage->references_header ?? [],
            );

            Mail::mailer($mailerName)->send($mailable);

            return true;
        } finally {
            // Restore original config (or remove the key entirely if it didn't exist).
            // Config::set($key, null) leaves a null entry in the array which can leak
            // the mailer name to subsequent resolution attempts on long-lived processes.
            $this->resetMailerConfig($mailerKey, $mailerName, $originalConfig);
        }
    }

    /**
     * Send via Gmail API.
     */
    protected function sendViaGmail(Message $replyMessage, EmailAccount $account, string $bodyHtml): bool
    {
        $client = new GoogleClient();
        $client->setClientId(config('services.google.client_id'));
        $client->setClientSecret(config('services.google.client_secret'));
        $client->setAccessToken([
            'access_token' => $account->oauth_token,
            'refresh_token' => $account->oauth_refresh_token,
            'expires_in' => 3600,
        ]);

        // Refresh token if expired
        if ($client->isAccessTokenExpired()) {
            $newToken = $client->fetchAccessTokenWithRefreshToken($account->oauth_refresh_token);
            if (isset($newToken['access_token'])) {
                $account->update([
                    'oauth_token' => $newToken['access_token'],
                    'oauth_token_expires_at' => now()->addSeconds($newToken['expires_in'] ?? 3600),
                ]);
            }
        }

        $gmail = new Gmail($client);

        // Build raw RFC 2822 message
        $rawMessage = $this->buildRawRfc2822($replyMessage, $account, $bodyHtml);

        $gmailMessage = new Gmail\Message();
        $gmailMessage->setRaw($this->base64UrlEncode($rawMessage));

        // If it's a reply, set threadId to keep conversation grouped
        $conversation = $replyMessage->conversation;
        if ($conversation) {
            $originalMessage = $conversation->messages()
                ->whereNotNull('channel_message_id')
                ->orderBy('created_at', 'asc')
                ->first();

            if ($originalMessage?->channel_message_id) {
                try {
                    $origGmailMsg = $gmail->users_messages->get('me', $originalMessage->channel_message_id, ['format' => 'metadata']);
                    $gmailMessage->setThreadId($origGmailMsg->getThreadId());
                } catch (\Throwable $e) {
                    Log::warning("EmailSend: Could not get Gmail thread ID: {$e->getMessage()}");
                }
            }
        }

        $result = $gmail->users_messages->send('me', $gmailMessage);

        $replyMessage->update(['channel_message_id' => $result->getId()]);

        return true;
    }

    /**
     * Send via Microsoft Graph API.
     */
    protected function sendViaOutlook(Message $replyMessage, EmailAccount $account, string $bodyHtml): bool
    {
        $httpClient = new \GuzzleHttp\Client();
        $accessToken = $account->oauth_token;

        $signature = $this->getSignature($account, $replyMessage);
        $fullHtml = $bodyHtml . ($signature ? "<br>{$signature}" : '');

        $payload = [
            'message' => [
                'subject' => $replyMessage->subject ?? $replyMessage->conversation?->subject ?? '(no subject)',
                'body' => [
                    'contentType' => 'HTML',
                    'content' => $fullHtml,
                ],
                'toRecipients' => collect($replyMessage->to_emails ?? [])->map(fn ($email) => [
                    'emailAddress' => ['address' => $email],
                ])->toArray(),
                'ccRecipients' => collect($replyMessage->cc_emails ?? [])->map(fn ($email) => [
                    'emailAddress' => ['address' => $email],
                ])->toArray(),
                'bccRecipients' => collect($replyMessage->bcc_emails ?? [])->map(fn ($email) => [
                    'emailAddress' => ['address' => $email],
                ])->toArray(),
            ],
            'saveToSentItems' => true,
        ];

        // Add threading headers for reply threading
        if ($replyMessage->in_reply_to) {
            $payload['message']['internetMessageHeaders'] = [
                ['name' => 'In-Reply-To', 'value' => $replyMessage->in_reply_to],
            ];
            if (! empty($replyMessage->references_header)) {
                $payload['message']['internetMessageHeaders'][] = [
                    'name' => 'References',
                    'value' => implode(' ', $replyMessage->references_header),
                ];
            }
        }

        // Add attachments (with size guard to prevent OOM)
        $attachmentPaths = $this->getAttachmentPaths($replyMessage);
        $maxAttachmentSize = 25 * 1024 * 1024; // 25 MB
        if (! empty($attachmentPaths)) {
            $payload['message']['attachments'] = [];
            foreach ($attachmentPaths as $path) {
                if (file_exists($path['path']) && filesize($path['path']) <= $maxAttachmentSize) {
                    $payload['message']['attachments'][] = [
                        '@odata.type' => '#microsoft.graph.fileAttachment',
                        'name' => $path['name'],
                        'contentType' => $path['mime'],
                        'contentBytes' => base64_encode(file_get_contents($path['path'])),
                    ];
                }
            }
        }

        $response = $httpClient->post('https://graph.microsoft.com/v1.0/me/sendMail', [
            'headers' => [
                'Authorization' => "Bearer {$accessToken}",
                'Content-Type' => 'application/json',
            ],
            'json' => $payload,
        ]);

        return $response->getStatusCode() === 202;
    }

    /**
     * Send a one-off email through a specific EmailAccount.
     *
     * Dispatches to the right transport based on provider — Gmail OAuth
     * accounts have no SMTP credentials so the old SMTP-only path crashed
     * with "Dsn host must be string, null given". Mirrors sendReply's
     * provider-aware routing but accepts primitive to/subject/body args.
     */
    public function send(
        EmailAccount $account,
        string $to,
        string $subject,
        string $htmlBody,
        array $options = []
    ): void {
        try {
            match ($account->provider) {
                'gmail' => $this->simpleSendGmail($account, $to, $subject, $htmlBody, $options),
                'outlook' => $this->simpleSendOutlook($account, $to, $subject, $htmlBody, $options),
                'imap', 'custom' => $this->simpleSendSmtp($account, $to, $subject, $htmlBody, $options),
                default => throw new \InvalidArgumentException("Unsupported provider: {$account->provider}"),
            };

            Log::info('Email sent successfully', [
                'account_id' => $account->id,
                'provider' => $account->provider,
                'to' => $to,
                'subject' => $subject,
            ]);
        } catch (\Throwable $e) {
            Log::error('Email send failed', [
                'account_id' => $account->id,
                'provider' => $account->provider,
                'to' => $to,
                'error' => $e->getMessage(),
            ]);

            throw new \RuntimeException(
                "Failed to send email via account {$account->email}: {$e->getMessage()}",
                0,
                $e
            );
        }
    }

    /**
     * Simple Gmail API send for one-off emails (campaign tests, notifications).
     */
    protected function simpleSendGmail(
        EmailAccount $account,
        string $to,
        string $subject,
        string $htmlBody,
        array $options
    ): void {
        $client = new GoogleClient();
        $client->setClientId(config('services.google.client_id'));
        $client->setClientSecret(config('services.google.client_secret'));
        $client->setAccessToken([
            'access_token' => $account->oauth_token,
            'refresh_token' => $account->oauth_refresh_token,
            'expires_in' => 3600,
        ]);

        if ($client->isAccessTokenExpired()) {
            $newToken = $client->fetchAccessTokenWithRefreshToken($account->oauth_refresh_token);
            if (isset($newToken['access_token'])) {
                $account->update([
                    'oauth_token' => $newToken['access_token'],
                    'oauth_token_expires_at' => now()->addSeconds($newToken['expires_in'] ?? 3600),
                ]);
                $client->setAccessToken([
                    'access_token' => $newToken['access_token'],
                    'refresh_token' => $account->oauth_refresh_token,
                    'expires_in' => $newToken['expires_in'] ?? 3600,
                ]);
            }
        }

        $gmail = new Gmail($client);

        $raw = $this->buildSimpleRfc2822($account, $to, $subject, $htmlBody, $options);

        $gmailMessage = new Gmail\Message();
        $gmailMessage->setRaw($this->base64UrlEncode($raw));
        $gmail->users_messages->send('me', $gmailMessage);
    }

    /**
     * Simple Outlook/Graph send for one-off emails.
     */
    protected function simpleSendOutlook(
        EmailAccount $account,
        string $to,
        string $subject,
        string $htmlBody,
        array $options
    ): void {
        $httpClient = new \GuzzleHttp\Client();

        $payload = [
            'message' => [
                'subject' => $subject,
                'body' => ['contentType' => 'HTML', 'content' => $htmlBody],
                'toRecipients' => [['emailAddress' => ['address' => $to]]],
            ],
            'saveToSentItems' => true,
        ];

        if (! empty($options['cc'])) {
            $payload['message']['ccRecipients'] = collect((array) $options['cc'])
                ->map(fn ($e) => ['emailAddress' => ['address' => $e]])->all();
        }
        if (! empty($options['bcc'])) {
            $payload['message']['bccRecipients'] = collect((array) $options['bcc'])
                ->map(fn ($e) => ['emailAddress' => ['address' => $e]])->all();
        }
        if (! empty($options['headers'])) {
            $payload['message']['internetMessageHeaders'] = [];
            foreach ($options['headers'] as $name => $value) {
                $payload['message']['internetMessageHeaders'][] = ['name' => $name, 'value' => $value];
            }
        }

        $httpClient->post('https://graph.microsoft.com/v1.0/me/sendMail', [
            'headers' => [
                'Authorization' => "Bearer {$account->oauth_token}",
                'Content-Type' => 'application/json',
            ],
            'json' => $payload,
        ]);
    }

    /**
     * Simple SMTP send for IMAP/custom accounts. Preserves the original
     * dynamic-mailer config dance used by the older send() path.
     */
    protected function simpleSendSmtp(
        EmailAccount $account,
        string $to,
        string $subject,
        string $htmlBody,
        array $options
    ): void {
        $displayName = $account->display_name ?? config('app.name');
        $mailerName = "campaign_{$account->id}";
        $mailerKey = "mail.mailers.{$mailerName}";
        $timeout = $account->smtp_timeout ?? config('mail.timeout', 30);

        $originalConfig = config($mailerKey);

        Config::set($mailerKey, SmtpConfig::build([
            'host' => $account->smtp_host,
            'port' => $account->smtp_port,
            'encryption' => $account->smtp_encryption,
            'username' => $account->smtp_username,
            'password' => $account->smtp_password,
            'timeout' => $timeout,
            'local_domain' => parse_url(config('app.url'), PHP_URL_HOST),
        ]));

        try {
            Mail::mailer($mailerName)
                ->html($htmlBody, function (\Illuminate\Mail\Message $message) use ($account, $to, $subject, $displayName, $options) {
                    $message->from($account->email, $displayName)
                        ->to($to)
                        ->subject($subject);

                    if (! empty($options['cc'])) {
                        $message->cc($options['cc']);
                    }
                    if (! empty($options['bcc'])) {
                        $message->bcc($options['bcc']);
                    }
                    if (! empty($options['replyTo'])) {
                        $message->replyTo($options['replyTo']);
                    }
                    if (! empty($options['headers'])) {
                        $symfonyMessage = $message->getSymfonyMessage();
                        foreach ($options['headers'] as $name => $value) {
                            $symfonyMessage->getHeaders()->addTextHeader($name, $value);
                        }
                    }
                    // Campaign attachments — file paths resolved to
                    // absolute disk locations by the caller (Job /
                    // controller). Each entry is either a string path
                    // or ['path' => ..., 'as' => filename, 'mime' => mime].
                    if (! empty($options['attachments']) && is_array($options['attachments'])) {
                        foreach ($options['attachments'] as $att) {
                            if (is_string($att) && is_file($att)) {
                                $message->attach($att);
                            } elseif (is_array($att) && !empty($att['path']) && is_file($att['path'])) {
                                $attachOpts = [];
                                if (!empty($att['as'])) $attachOpts['as'] = $att['as'];
                                if (!empty($att['mime'])) $attachOpts['mime'] = $att['mime'];
                                $message->attach($att['path'], $attachOpts);
                            }
                        }
                    }
                });
        } finally {
            $this->resetMailerConfig($mailerKey, $mailerName, $originalConfig);
        }
    }

    /**
     * Build a minimal RFC 2822 message for a one-off send (no reply threading
     * headers, no attachments). Used by the Gmail simple-send path.
     */
    protected function buildSimpleRfc2822(
        EmailAccount $account,
        string $to,
        string $subject,
        string $htmlBody,
        array $options
    ): string {
        $fromName = str_replace(['"', "\r", "\n"], '', $account->display_name ?? $account->email);
        $fromEmail = $account->email;

        $headers = [
            "From: \"{$fromName}\" <{$fromEmail}>",
            "To: {$to}",
        ];

        if (! empty($options['cc'])) {
            $cc = is_array($options['cc']) ? implode(', ', $options['cc']) : $options['cc'];
            $headers[] = "Cc: {$cc}";
        }
        if (! empty($options['bcc'])) {
            $bcc = is_array($options['bcc']) ? implode(', ', $options['bcc']) : $options['bcc'];
            $headers[] = "Bcc: {$bcc}";
        }
        if (! empty($options['replyTo'])) {
            $headers[] = "Reply-To: {$options['replyTo']}";
        }

        $headers[] = "Subject: {$subject}";
        $headers[] = 'MIME-Version: 1.0';
        $headers[] = 'Content-Type: text/html; charset="UTF-8"';
        $headers[] = 'Content-Transfer-Encoding: base64';

        if (! empty($options['headers'])) {
            foreach ($options['headers'] as $name => $value) {
                $headers[] = "{$name}: {$value}";
            }
        }

        return implode("\r\n", $headers) . "\r\n\r\n" . chunk_split(base64_encode($htmlBody));
    }

    /**
     * Reset a dynamically created mailer config and purge its cached transport.
     *
     * Config::set($key, null) does NOT remove the key from the config repository --
     * it leaves a null entry that can confuse Laravel's MailManager on long-lived
     * processes (queue workers). This method properly restores the original value
     * and purges the cached Symfony transport so credentials are never retained.
     */
    protected function resetMailerConfig(string $configKey, string $mailerName, mixed $originalConfig): void
    {
        // Restore the original config value. If the key didn't exist before
        // (originalConfig is null), we remove the entire mailer entry from
        // the config array so it doesn't linger with null credentials.
        if ($originalConfig === null) {
            $allMailers = config('mail.mailers', []);
            unset($allMailers[$mailerName]);
            Config::set('mail.mailers', $allMailers);
        } else {
            Config::set($configKey, $originalConfig);
        }

        // Purge the cached mailer/transport instance from the MailManager so
        // subsequent calls don't reuse a transport with stale credentials.
        Mail::purge($mailerName);
    }

    /**
     * Inject open-tracking pixel and click-tracking links into the HTML body.
     */
    protected function injectTracking(Message $message): string
    {
        $bodyHtml = $message->body_html ?? '';
        $appUrl = config('app.url');
        $uuid = $message->uuid;

        // Inject open tracking pixel before closing </body> tag
        $trackingPixel = "<img src=\"{$appUrl}/api/track/open/{$uuid}\" width=\"1\" height=\"1\" style=\"display:none;\" alt=\"\" />";

        if (stripos($bodyHtml, '</body>') !== false) {
            $bodyHtml = str_ireplace('</body>', $trackingPixel . '</body>', $bodyHtml);
        } else {
            $bodyHtml .= $trackingPixel;
        }

        // Wrap all <a href="..."> links for click tracking
        $bodyHtml = preg_replace_callback(
            '/<a\s+([^>]*?)href=["\']([^"\']+?)["\']([^>]*?)>/i',
            function ($matches) use ($appUrl, $uuid) {
                $originalUrl = $matches[2];

                // Skip tracking URLs, mailto, tel, and anchor links
                if (str_contains($originalUrl, '/api/track/') ||
                    str_starts_with($originalUrl, 'mailto:') ||
                    str_starts_with($originalUrl, 'tel:') ||
                    str_starts_with($originalUrl, '#')) {
                    return $matches[0];
                }

                // Only track valid HTTP(S) URLs -- skip malformed or protocol-relative URLs
                if (!filter_var($originalUrl, FILTER_VALIDATE_URL) || !str_starts_with($originalUrl, 'http')) {
                    return $matches[0];
                }

                $trackedUrl = "{$appUrl}/api/track/click/{$uuid}?url=" . urlencode($originalUrl);

                return "<a {$matches[1]}href=\"{$trackedUrl}\"{$matches[3]}>";
            },
            $bodyHtml
        );

        return $bodyHtml;
    }

    /**
     * Append "Was this helpful?" thumbs links to AI replies that have
     * a Knowledge Agent feedback token.
     *
     * Skips when:
     *   - the Message isn't AI-authored (no token to bind to)
     *   - was_fallback fired (no real answer to rate)
     *   - feedback link already injected (idempotency guard for retries)
     *
     * The two links go directly to /kb/feedback/{token}/{verdict} so
     * the customer's click is one round-trip — no JavaScript, works
     * in Outlook / Apple Mail / Gmail without a webview.
     */
    protected function injectKbFeedback(Message $message, string $bodyHtml): string
    {
        if ($message->sender_type !== 'ai') return $bodyHtml;
        if (!$message->ai_feedback_token) return $bodyHtml;
        if ($message->ai_was_fallback) return $bodyHtml;
        if (str_contains($bodyHtml, 'data-kb-feedback')) return $bodyHtml;

        $upUrl = route('kb.feedback', ['token' => $message->ai_feedback_token, 'verdict' => 'up']);
        $downUrl = route('kb.feedback', ['token' => $message->ai_feedback_token, 'verdict' => 'down']);

        $block = '<div data-kb-feedback="1" style="margin-top:24px;padding-top:16px;border-top:1px solid #e5e7eb;font-family:Arial,Helvetica,sans-serif;font-size:13px;color:#6b7280;">'
            . '<span style="margin-right:10px;">Was this helpful?</span>'
            . '<a href="' . e($upUrl) . '" style="display:inline-block;padding:6px 12px;margin-right:8px;background:#f3f4f6;border:1px solid #d1d5db;border-radius:6px;color:#111827;text-decoration:none;">&#128077; Yes</a>'
            . '<a href="' . e($downUrl) . '" style="display:inline-block;padding:6px 12px;background:#f3f4f6;border:1px solid #d1d5db;border-radius:6px;color:#111827;text-decoration:none;">&#128078; No</a>'
            . '</div>';

        if (stripos($bodyHtml, '</body>') !== false) {
            return str_ireplace('</body>', $block . '</body>', $bodyHtml);
        }
        return $bodyHtml . $block;
    }

    /**
     * Get the email signature for the account.
     */
    protected function getSignature(EmailAccount $account, Message $message): ?string
    {
        $isReply = ! empty($message->in_reply_to);

        $signature = $account->emailSignatures()
            ->where('is_default', true)
            ->where($isReply ? 'append_to_replies' : 'append_to_new', true)
            ->first();

        return $signature?->content_html;
    }

    /**
     * Get attachment file paths for a message.
     *
     * @return array<array{path: string, name: string, mime: string}>
     */
    protected function getAttachmentPaths(Message $message): array
    {
        $disk = \Illuminate\Support\Facades\Storage::disk('local');

        return $message->attachments->map(function ($att) use ($disk) {
            // Resolve via the disk so we always match whatever root is configured
            // (Laravel 11's default local disk is rooted at storage/app/private,
            // not storage/app — a direct storage_path('app/...') lookup misses it).
            $path = $disk->path($att->storage_path);

            // Back-compat: fall back to legacy storage_path('app/...') if the
            // file isn't on the local disk (older rows written before the disk
            // root changed).
            if (! file_exists($path)) {
                $legacy = storage_path("app/{$att->storage_path}");
                if (file_exists($legacy)) {
                    $path = $legacy;
                }
            }

            return [
                'path' => $path,
                'name' => $att->original_filename ?? $att->filename,
                'mime' => $att->mime_type,
            ];
        })->toArray();
    }

    /**
     * Build an RFC 2822 raw email string for Gmail API.
     */
    protected function buildRawRfc2822(Message $message, EmailAccount $account, string $bodyHtml): string
    {
        $boundary = Str::random(32);
        $fromName = str_replace(['"', "\r", "\n"], '', $message->from_name ?? $account->display_name ?? '');
        $fromEmail = $message->from_email ?? $account->email;
        $to = implode(', ', $message->to_emails ?? []);
        $cc = implode(', ', $message->cc_emails ?? []);

        $signature = $this->getSignature($account, $message);
        $fullHtml = $bodyHtml . ($signature ? "<br>{$signature}" : '');

        $headers = [
            "From: \"{$fromName}\" <{$fromEmail}>",
            "To: {$to}",
        ];

        if ($cc) {
            $headers[] = "Cc: {$cc}";
        }

        $headers[] = 'Subject: ' . ($message->subject ?? $message->conversation?->subject ?? '(no subject)');
        $headers[] = 'MIME-Version: 1.0';

        // Threading headers for reply chains
        if ($message->in_reply_to) {
            $headers[] = "In-Reply-To: {$message->in_reply_to}";
        }
        if (! empty($message->references_header)) {
            $headers[] = 'References: ' . implode(' ', $message->references_header);
        }

        $headers[] = "Content-Type: multipart/mixed; boundary=\"{$boundary}\"";
        $headers[] = '';

        $raw = implode("\r\n", $headers);
        $raw .= "\r\n--{$boundary}\r\n";
        $raw .= "Content-Type: text/html; charset=\"UTF-8\"\r\n";
        $raw .= "Content-Transfer-Encoding: base64\r\n\r\n";
        $raw .= chunk_split(base64_encode($fullHtml));

        // Add file attachments (with size guard to prevent OOM)
        $maxAttachmentSize = 25 * 1024 * 1024; // 25 MB
        foreach ($this->getAttachmentPaths($message) as $att) {
            if (file_exists($att['path']) && filesize($att['path']) <= $maxAttachmentSize) {
                $raw .= "\r\n--{$boundary}\r\n";
                $raw .= "Content-Type: {$att['mime']}; name=\"{$att['name']}\"\r\n";
                $raw .= "Content-Disposition: attachment; filename=\"{$att['name']}\"\r\n";
                $raw .= "Content-Transfer-Encoding: base64\r\n\r\n";
                $raw .= chunk_split(base64_encode(file_get_contents($att['path'])));
            }
        }

        $raw .= "\r\n--{$boundary}--";

        return $raw;
    }

    /**
     * Base64url encode for Gmail API.
     */
    protected function base64UrlEncode(string $data): string
    {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }
}
