<?php
require_once __DIR__ . '/models/Message.php';
require_once __DIR__ . '/models/Contact.php';
require_once __DIR__ . '/models/Template.php';
require_once __DIR__ . '/models/Campaign.php';
require_once __DIR__ . '/models/QueueJob.php';
require_once __DIR__ . '/models/Suppression.php';
require_once __DIR__ . '/models/ContactList.php';

class TwilioService
{
    public function sendSms(string $to, string $body, ?string $mediaUrl = null): array
    {
        $url = 'https://api.twilio.com/2010-04-01/Accounts/' . effective_twilio_config()['sid'] . '/Messages.json';
        $payload = [
            'To' => $to,
            'From' => effective_twilio_config()['from_number'],
            'Body' => $body,
            'StatusCallback' => full_url('index.php?route=webhook/twilioStatus'),
        ];
        if ($mediaUrl) {
            $payload['MediaUrl'] = $mediaUrl;
        }
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => http_build_query($payload),
            CURLOPT_USERPWD => effective_twilio_config()['sid'] . ':' . effective_twilio_config()['token'],
        ]);
        $response = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);
        if ($error) {
            throw new RuntimeException('Twilio error: ' . $error);
        }
        $decoded = json_decode((string)$response, true) ?: [];
        if ($status >= 400) {
            throw new RuntimeException('Twilio API failed: ' . ($decoded['message'] ?? $response));
        }
        return $decoded;
    }

    public function validateRequest(string $url, array $params, ?string $signature): bool
    {
        if (!effective_twilio_config()['validate_signature']) {
            return true;
        }
        $token = (string)effective_twilio_config()['token'];
        if ($token === '' || !$signature) {
            return false;
        }
        ksort($params);
        $data = $url;
        foreach ($params as $key => $value) {
            if (!is_array($value)) {
                $data .= $key . $value;
            }
        }
        $expected = base64_encode(hash_hmac('sha1', $data, $token, true));
        return hash_equals($expected, $signature);
    }
}

class EmailService
{
    public function sendEmail(string $to, string $subject, string $html, string $text = '', array $attachments = [], array $customHeaders = []): bool
    {
        if (effective_mail_config()['smtp_enabled']) {
            return $this->sendViaSmtp($to, $subject, $html, $text, $attachments, $customHeaders);
        }
        return $this->sendViaPhpMail($to, $subject, $html, $attachments, $customHeaders);
    }

    public function sendPasswordReset(string $to, string $name, string $resetUrl): bool
    {
        $subject = 'Password reset request';
        $html = '<p>Hello ' . e($name) . ',</p><p>Use the link below to reset your password:</p><p><a href="' . e($resetUrl) . '">' . e($resetUrl) . '</a></p><p>This link expires in 60 minutes.</p>';
        $text = "Hello {$name},\n\nReset your password here:\n{$resetUrl}\n\nThis link expires in 60 minutes.";
        return $this->sendEmail($to, $subject, $html, $text);
    }

    private function sendViaPhpMail(string $to, string $subject, string $html, array $attachments = [], array $customHeaders = []): bool
    {
        [$headers, $body] = $this->buildMimeMessage($html, strip_tags($html), $attachments);
        $mailConfig = effective_mail_config();
        $headers[] = 'From: ' . $mailConfig['from_name'] . ' <' . $mailConfig['from_email'] . '>';
        if (!empty($mailConfig['reply_to_email'])) { $headers[] = 'Reply-To: ' . $mailConfig['reply_to_email']; }
        foreach ($customHeaders as $line) {
            $headers[] = $line;
        }
        return mail($to, $subject, $body, implode("\r\n", $headers));
    }

    private function sendViaSmtp(string $to, string $subject, string $html, string $text = '', array $attachments = [], array $customHeaders = []): bool
    {
        $mailConfig = effective_mail_config();
        $host = $mailConfig['smtp_host'];
        $port = (int)$mailConfig['smtp_port'];
        $secure = $mailConfig['smtp_secure'];
        $transport = $secure === 'ssl' ? 'ssl://' . $host : $host;
        $socket = stream_socket_client($transport . ':' . $port, $errno, $errstr, 30, STREAM_CLIENT_CONNECT);
        if (!$socket) {
            throw new RuntimeException('SMTP connection failed: ' . $errstr);
        }
        $this->smtpExpect($socket, [220]);
        $this->smtpWrite($socket, 'EHLO localhost');
        $this->smtpExpect($socket, [250]);
        if ($secure === 'tls') {
            $this->smtpWrite($socket, 'STARTTLS');
            $this->smtpExpect($socket, [220]);
            stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
            $this->smtpWrite($socket, 'EHLO localhost');
            $this->smtpExpect($socket, [250]);
        }
        $username = $mailConfig['smtp_username'];
        $password = $mailConfig['smtp_password'];
        if ($username) {
            $this->smtpWrite($socket, 'AUTH LOGIN');
            $this->smtpExpect($socket, [334]);
            $this->smtpWrite($socket, base64_encode($username));
            $this->smtpExpect($socket, [334]);
            $this->smtpWrite($socket, base64_encode($password));
            $this->smtpExpect($socket, [235]);
        }
        $from = $mailConfig['from_email'];
        $this->smtpWrite($socket, 'MAIL FROM:<' . $from . '>');
        $this->smtpExpect($socket, [250]);
        $this->smtpWrite($socket, 'RCPT TO:<' . $to . '>');
        $this->smtpExpect($socket, [250, 251]);
        $this->smtpWrite($socket, 'DATA');
        $this->smtpExpect($socket, [354]);
        [$headers, $body] = $this->buildMimeMessage($html, $text ?: strip_tags($html), $attachments);
        $headers[] = 'From: ' . $mailConfig['from_name'] . ' <' . $from . '>';
        if (!empty($mailConfig['reply_to_email'])) { $headers[] = 'Reply-To: ' . $mailConfig['reply_to_email']; }
        $headers[] = 'To: <' . $to . '>';
        $headers[] = 'Subject: ' . $subject;
        $headers[] = 'Date: ' . date(DATE_RFC2822);
        foreach ($customHeaders as $line) {
            $headers[] = $line;
        }
        fwrite($socket, implode("\r\n", $headers) . "\r\n\r\n" . $body . "\r\n.\r\n");
        $this->smtpExpect($socket, [250]);
        $this->smtpWrite($socket, 'QUIT');
        fclose($socket);
        return true;
    }

    private function buildMimeMessage(string $html, string $text, array $attachments = []): array
    {
        $mixedBoundary = 'mix_' . md5((string)microtime(true));
        $altBoundary = 'alt_' . md5((string)mt_rand());
        $headers = ['MIME-Version: 1.0'];
        if ($attachments) {
            $headers[] = 'Content-Type: multipart/mixed; boundary="' . $mixedBoundary . '"';
            $body = '--' . $mixedBoundary . "\r\n";
            $body .= 'Content-Type: multipart/alternative; boundary="' . $altBoundary . '"' . "\r\n\r\n";
        } else {
            $headers[] = 'Content-Type: multipart/alternative; boundary="' . $altBoundary . '"';
            $body = '';
        }
        $body .= '--' . $altBoundary . "\r\n";
        $body .= "Content-Type: text/plain; charset=UTF-8\r\n\r\n" . $text . "\r\n";
        $body .= '--' . $altBoundary . "\r\n";
        $body .= "Content-Type: text/html; charset=UTF-8\r\n\r\n" . $html . "\r\n";
        $body .= '--' . $altBoundary . "--\r\n";
        if ($attachments) {
            foreach ($attachments as $path) {
                if (!is_file($path)) {
                    continue;
                }
                $filename = basename($path);
                $content = chunk_split(base64_encode((string)file_get_contents($path)));
                $body .= '--' . $mixedBoundary . "\r\n";
                $body .= 'Content-Type: application/octet-stream; name="' . $filename . '"' . "\r\n";
                $body .= "Content-Transfer-Encoding: base64\r\n";
                $body .= 'Content-Disposition: attachment; filename="' . $filename . '"' . "\r\n\r\n";
                $body .= $content . "\r\n";
            }
            $body .= '--' . $mixedBoundary . "--\r\n";
        }
        return [$headers, $body];
    }

    private function smtpWrite($socket, string $line): void
    {
        fwrite($socket, $line . "\r\n");
    }

    private function smtpExpect($socket, array $codes): void
    {
        $response = '';
        while (($line = fgets($socket, 515)) !== false) {
            $response .= $line;
            if (isset($line[3]) && $line[3] === ' ') {
                break;
            }
        }
        $code = (int)substr($response, 0, 3);
        if (!in_array($code, $codes, true)) {
            throw new RuntimeException('SMTP error: ' . trim($response));
        }
    }

    public function syncImap(): int
    {
        $mailbox = sprintf('{%s:%d%s}%s', setting_value('imap_host', config('imap.host')), setting_value('imap_port', config('imap.port')), setting_value('imap_flags', config('imap.flags')), setting_value('imap_mailbox', config('imap.mailbox')));
        $inbox = @imap_open($mailbox, setting_value('imap_username', config('imap.username')), setting_value('imap_password', config('imap.password')));
        if (!$inbox) {
            throw new RuntimeException('Unable to connect to IMAP mailbox.');
        }
        $emails = imap_search($inbox, 'ALL') ?: [];
        $messageModel = new Message();
        $contactModel = new Contact();
        $count = 0;
        foreach ($emails as $emailNumber) {
            $overview = imap_fetch_overview($inbox, (string)$emailNumber, 0)[0] ?? null;
            if (!$overview) {
                continue;
            }
            $body = imap_fetchbody($inbox, $emailNumber, '1') ?: imap_body($inbox, $emailNumber);
            $messageId = $overview->message_id ?? null;
            if (!$messageId) {
                continue;
            }
            $db = Database::connection();
            $check = $db->prepare('SELECT id FROM messages WHERE external_id = ? AND channel = "email" LIMIT 1');
            $check->execute([$messageId]);
            if ($check->fetch()) {
                continue;
            }
            $headerInfo = imap_headerinfo($inbox, $emailNumber);
            $from = $overview->from ?? '';
            preg_match('/<([^>]+)>/', $from, $matches);
            $emailAddress = $matches[1] ?? trim($from);
            $contact = $contactModel->findByAddress('email', $emailAddress);
            $references = isset($headerInfo->references) ? trim((string)$headerInfo->references) : null;
            $inReplyTo = isset($headerInfo->in_reply_to) ? trim((string)$headerInfo->in_reply_to) : null;
            $threadKey = $references ?: $inReplyTo ?: normalize_address($emailAddress);
            $messageModel->create([
                'contact_id' => $contact['id'] ?? null,
                'channel' => 'email',
                'direction' => 'inbound',
                'subject' => $overview->subject ?? '',
                'body' => quoted_printable_decode($body),
                'from_address' => $from,
                'to_address' => $overview->to ?? '',
                'status' => 'received',
                'external_id' => $messageId,
                'thread_key' => 'email:thread:' . md5($threadKey),
                'in_reply_to_external_id' => $inReplyTo,
                'references_header' => $references,
            ]);
            $count++;
        }
        imap_close($inbox);
        return $count;
    }
}

class CampaignService
{
    public function queueCampaign(array $data): array
    {
        $campaignModel = new Campaign();
        $contactModel = new Contact();
        $queueJobModel = new QueueJob();
        $scheduledAt = trim($data['scheduled_at'] ?? '') ?: null;
        $submitMode = $data['submit_mode'] ?? 'queue';
        $approvalStatus = $submitMode === 'approval' ? 'pending' : 'not_required';
        $status = $submitMode === 'draft' ? 'draft' : 'queued';
        $payload = [
            'name' => trim($data['name'] ?? 'Untitled Campaign'),
            'channel' => $data['channel'] ?? 'sms',
            'template_id' => !empty($data['template_id']) ? (int)$data['template_id'] : null,
            'status' => $status,
            'recipient_filter' => trim($data['recipient_filter'] ?? '') ?: null,
            'scheduled_at' => $scheduledAt,
            'created_by' => current_user()['id'] ?? null,
            'subject_override' => trim($data['subject_override'] ?? '') ?: null,
            'body_override' => trim($data['body_override'] ?? '') ?: null,
            'segment_name' => trim($data['segment_name'] ?? '') ?: null,
            'segment_id' => !empty($data['segment_id']) ? (int)$data['segment_id'] : null,
            'contact_list_id' => !empty($data['contact_list_id']) ? (int)$data['contact_list_id'] : null,
            'next_occurrence_at' => !empty($data['recurring_enabled']) ? $this->nextRecurringAt((string)($data['recurring_rule'] ?? 'daily'), $scheduledAt ?: date('Y-m-d H:i:s')) : null,
            'approval_status' => $approvalStatus,
            'ab_enabled' => !empty($data['ab_enabled']) ? 1 : 0,
            'variant_a_subject' => trim($data['variant_a_subject'] ?? '') ?: null,
            'variant_a_body' => trim($data['variant_a_body'] ?? '') ?: null,
            'variant_b_subject' => trim($data['variant_b_subject'] ?? '') ?: null,
            'variant_b_body' => trim($data['variant_b_body'] ?? '') ?: null,
            'recurring_enabled' => !empty($data['recurring_enabled']) ? 1 : 0,
            'recurring_rule' => trim($data['recurring_rule'] ?? '') ?: null,
            'send_time_optimization' => !empty($data['send_time_optimization']) ? 1 : 0,
            'builder_json' => trim($data['builder_json'] ?? '') ?: null,
        ];
        if (($payload['body_override'] ?? null) === null && !empty($payload['builder_json'])) {
            $payload['body_override'] = $this->renderBuilderHtml($payload['builder_json']);
        }

        $template = !empty($payload['template_id']) ? (new Template())->find((int)$payload['template_id']) : null;
        $contactListId = !empty($payload['contact_list_id']) ? (int)$payload['contact_list_id'] : null;
        $contactList = $contactListId ? (new ContactList())->find($contactListId) : null;
        $segmentId = !empty($payload['segment_id']) ? (int)$payload['segment_id'] : null;
        $contacts = $contactListId
            ? (new ContactList())->contactsForChannel($contactListId, $payload['channel'])
            : $contactModel->activeByChannel($payload['channel'], $payload['recipient_filter'], $segmentId);

        $blockingMode = $submitMode !== 'draft';
        $preflight = $this->buildPreflight($payload, count($contacts), $template, $contactList, $blockingMode);
        if (!empty($preflight['errors'])) {
            throw new RuntimeException(implode(' ', $preflight['errors']));
        }

        $campaignId = $campaignModel->create($payload);
        $campaignModel->seedRecipients($campaignId, $contacts, $payload['channel']);
        if ($status === 'queued' && $approvalStatus !== 'pending') {
            $queueJobModel->create('campaign_process', ['campaign_id' => $campaignId], $scheduledAt);
        }
        audit_log('campaign.saved', 'campaign', $campaignId, [
            'channel' => $payload['channel'],
            'recipients' => count($contacts),
            'scheduled_at' => $scheduledAt,
            'segment_id' => $segmentId,
            'contact_list_id' => $contactListId,
            'approval_status' => $approvalStatus,
            'submit_mode' => $submitMode,
            'ab_enabled' => $payload['ab_enabled'],
            'recurring_enabled' => $payload['recurring_enabled'],
            'next_occurrence_at' => $payload['next_occurrence_at'],
            'preflight_warnings' => $preflight['warnings'],
        ]);
        return ['id' => $campaignId, 'preflight' => $preflight];
    }

    public function preflightForCampaign(array $campaign): array
    {
        $template = !empty($campaign['template_id']) ? (new Template())->find((int)$campaign['template_id']) : null;
        $contactList = !empty($campaign['contact_list_id']) ? (new ContactList())->find((int)$campaign['contact_list_id']) : null;
        $blockingMode = ($campaign['status'] ?? 'queued') !== 'draft';
        return $this->buildPreflight($campaign, (int)($campaign['total_recipients'] ?? 0), $template, $contactList, $blockingMode);
    }

    private function buildPreflight(array $campaignLike, int $recipientCount, ?array $template = null, ?array $contactList = null, bool $blockingMode = true): array
    {
        $providerStatus = provider_status_summary();
        $systemHealth = (new Message())->healthSummary(30);
        $channel = (string)($campaignLike['channel'] ?? 'email');
        $subject = trim((string)($campaignLike['subject_override'] ?? ($template['subject'] ?? '')));
        $body = $this->baseCampaignBody($campaignLike, $template);
        $variantCount = 1 + ((trim((string)($campaignLike['variant_a_subject'] ?? '')) !== '' || trim((string)($campaignLike['variant_a_body'] ?? '')) !== '') ? 1 : 0) + ((trim((string)($campaignLike['variant_b_subject'] ?? '')) !== '' || trim((string)($campaignLike['variant_b_body'] ?? '')) !== '') ? 1 : 0);
        $blockCount = $this->builderBlockCount((string)($campaignLike['builder_json'] ?? ''));
        $audienceSource = !empty($campaignLike['contact_list_id'])
            ? ('Saved list: ' . ($contactList['name'] ?? 'selected list'))
            : (!empty($campaignLike['segment_id'])
                ? ('Dynamic segment #' . (int)$campaignLike['segment_id'])
                : (!empty($campaignLike['recipient_filter'])
                    ? ('Tag filter: ' . (string)$campaignLike['recipient_filter'])
                    : ('All reachable ' . strtoupper($channel) . ' contacts')));

        $errors = [];
        $warnings = [];
        $checks = [];
        $issue = function (string $message) use (&$errors, &$warnings, $blockingMode): void {
            if ($blockingMode) {
                $errors[] = $message;
            } else {
                $warnings[] = $message;
            }
        };

        $providerOk = $channel === 'sms' ? !empty($providerStatus['twilio']['configured']) : !empty($providerStatus['smtp']['configured']);
        if (!$providerOk) {
            $issue($channel === 'sms' ? 'Twilio is not configured for SMS campaigns.' : 'SMTP or mail-from settings are not configured for email campaigns.');
            $checks[] = ['label' => 'Provider readiness', 'status' => 'error', 'detail' => $channel === 'sms' ? 'Twilio missing' : 'SMTP missing'];
        } else {
            $checks[] = ['label' => 'Provider readiness', 'status' => 'pass', 'detail' => $channel === 'sms' ? 'Twilio ready' : 'SMTP ready'];
        }

        if ($channel === 'email' && $subject === '') {
            $issue('Email campaigns need a subject or a template subject before they can send.');
            $checks[] = ['label' => 'Subject line', 'status' => 'error', 'detail' => 'Missing subject'];
        } else {
            $checks[] = ['label' => 'Subject line', 'status' => 'pass', 'detail' => $channel === 'email' ? 'Subject ready' : 'Not required for SMS'];
        }

        if (trim(strip_tags($body)) === '') {
            $issue('Campaign content is empty. Add builder blocks, a template, or a body override.');
            $checks[] = ['label' => 'Content', 'status' => 'error', 'detail' => 'No message body detected'];
        } else {
            $checks[] = ['label' => 'Content', 'status' => 'pass', 'detail' => $blockCount > 0 ? ($blockCount . ' builder blocks ready') : 'Content body ready'];
        }

        if ($recipientCount <= 0) {
            $issue('This campaign has no reachable recipients based on the selected audience.');
            $checks[] = ['label' => 'Audience', 'status' => 'error', 'detail' => '0 reachable recipients'];
        } else {
            $checks[] = ['label' => 'Audience', 'status' => 'pass', 'detail' => number_format($recipientCount) . ' reachable recipients'];
        }

        if ($contactList && ($contactList['channel'] ?? 'both') !== 'both' && ($contactList['channel'] ?? 'both') !== $channel) {
            $issue('The selected contact list is restricted to ' . strtoupper((string)$contactList['channel']) . ' recipients and does not match this campaign channel.');
            $checks[] = ['label' => 'List/channel match', 'status' => 'error', 'detail' => strtoupper((string)$contactList['channel']) . ' list on ' . strtoupper($channel) . ' campaign'];
        } else {
            $checks[] = ['label' => 'Audience source', 'status' => 'pass', 'detail' => $audienceSource];
        }

        if ($channel === 'email') {
            $hasUnsubscribe = stripos($body, '{{unsubscribe_url}}') !== false || stripos($body, 'unsubscribe') !== false;
            if (!$hasUnsubscribe) {
                $warnings[] = 'Add an unsubscribe link or token to improve deliverability and compliance.';
                $checks[] = ['label' => 'Unsubscribe footer', 'status' => 'warn', 'detail' => 'No unsubscribe language detected'];
            } else {
                $checks[] = ['label' => 'Unsubscribe footer', 'status' => 'pass', 'detail' => 'Unsubscribe language detected'];
            }
        }

        if ((float)($systemHealth['bounce_rate'] ?? 0) >= 3.0 && $channel === 'email') {
            $warnings[] = 'Recent email bounce rate is elevated at ' . number_format((float)$systemHealth['bounce_rate'], 1) . '%. Review list hygiene before launch.';
        }
        if ((float)($systemHealth['queue_pressure'] ?? 0) >= 20.0) {
            $warnings[] = 'Queue backlog is elevated at ' . number_format((float)$systemHealth['queue_pressure'], 1) . '%. Processing may be delayed.';
        }

        return [
            'errors' => array_values(array_unique($errors)),
            'warnings' => array_values(array_unique($warnings)),
            'checks' => $checks,
            'summary' => [
                'recipient_count' => $recipientCount,
                'variant_count' => $variantCount,
                'block_count' => $blockCount,
                'audience_source' => $audienceSource,
                'channel' => strtoupper($channel),
            ],
        ];
    }

    private function renderBuilderHtml(?string $builderJson): string
    {
        $blocks = json_decode((string)$builderJson, true);
        if (!is_array($blocks)) {
            return '';
        }
        $html = [];
        foreach ($blocks as $block) {
            if (is_string($block)) {
                $value = trim($block);
            } else {
                $value = trim((string)($block['html'] ?? ($block['content'] ?? '')));
            }
            if ($value !== '') {
                $html[] = $value;
            }
        }
        return implode("

", $html);
    }

    private function builderBlockCount(?string $builderJson): int
    {
        $blocks = json_decode((string)$builderJson, true);
        return is_array($blocks) ? count($blocks) : 0;
    }

    private function baseCampaignBody(array $campaign, ?array $template): string
    {
        $body = trim((string)($campaign['body_override'] ?? ''));
        if ($body !== '') {
            return $body;
        }
        if (!empty($campaign['builder_json'])) {
            $builderBody = $this->renderBuilderHtml((string)$campaign['builder_json']);
            if (trim(strip_tags($builderBody)) !== '') {
                return $builderBody;
            }
        }
        return trim((string)($template['body'] ?? ''));
    }

    private function selectVariant(array $campaign, array $recipient): string
    {
        if (empty($campaign['ab_enabled'])) {
            return 'default';
        }
        return ((int)($recipient['id'] ?? 0) % 2 === 0) ? 'a' : 'b';
    }

    private function subjectForVariant(array $campaign, ?array $template, array $recipient, string $variant): string
    {
        $base = $campaign['subject_override'] ?? ($template['subject'] ?? $campaign['name']);
        if ($variant === 'a' && !empty($campaign['variant_a_subject'])) {
            $base = $campaign['variant_a_subject'];
        } elseif ($variant === 'b' && !empty($campaign['variant_b_subject'])) {
            $base = $campaign['variant_b_subject'];
        }
        return render_template_content((string)$base, $recipient);
    }

    private function bodyForVariant(array $campaign, ?array $template, array $recipient, string $variant): string
    {
        $base = $this->baseCampaignBody($campaign, $template);
        if ($variant === 'a' && !empty($campaign['variant_a_body'])) {
            $base = $campaign['variant_a_body'];
        } elseif ($variant === 'b' && !empty($campaign['variant_b_body'])) {
            $base = $campaign['variant_b_body'];
        }
        return render_template_content((string)$base, $recipient, [
            'unsubscribe_url' => full_url('index.php?route=messages/unsubscribe&channel=' . $campaign['channel'] . '&address=' . urlencode($recipient['address']))
        ]);
    }

    private function nextRecurringAt(string $rule, ?string $from = null): string
    {
        $base = new DateTimeImmutable($from ?: 'now', new DateTimeZone(config('app.timezone', 'America/Chicago')));
        $rule = strtolower(trim($rule));
        if ($rule === 'weekly') {
            return $base->modify('+1 week')->format('Y-m-d H:i:s');
        }
        if ($rule === 'monthly') {
            return $base->modify('+1 month')->format('Y-m-d H:i:s');
        }
        return $base->modify('+1 day')->format('Y-m-d H:i:s');
    }

    private function scheduleNextRecurringCampaign(array $campaign, Campaign $campaignModel): ?int
    {
        if (empty($campaign['recurring_enabled'])) {
            return null;
        }
        $contactModel = new Contact();
        $nextAt = $this->nextRecurringAt((string)($campaign['recurring_rule'] ?? 'daily'), $campaign['scheduled_at'] ?? null);
        $cloneData = $campaign;
        $cloneData['scheduled_at'] = $nextAt;
        $cloneData['next_occurrence_at'] = $this->nextRecurringAt((string)($campaign['recurring_rule'] ?? 'daily'), $nextAt);
        $cloneData['recurring_parent_id'] = !empty($campaign['recurring_parent_id']) ? (int)$campaign['recurring_parent_id'] : (int)$campaign['id'];
        $newId = $campaignModel->cloneForRecurring($cloneData);
        $freshContacts = !empty($campaign['contact_list_id'])
            ? (new ContactList())->contactsForChannel((int)$campaign['contact_list_id'], (string)$campaign['channel'])
            : $contactModel->activeByChannel((string)$campaign['channel'], $campaign['recipient_filter'] ?? null, !empty($campaign['segment_id']) ? (int)$campaign['segment_id'] : null);
        $campaignModel->seedRecipients($newId, $freshContacts, (string)$campaign['channel']);
        $campaignModel->updateNextOccurrence((int)$campaign['id'], $nextAt);
        (new QueueJob())->create('campaign_process', ['campaign_id' => $newId], $nextAt);
        audit_log('campaign.recurring.cloned', 'campaign', $newId, ['source_campaign_id' => (int)$campaign['id'], 'scheduled_at' => $nextAt, 'recipient_count' => count($freshContacts)]);
        return $newId;
    }

    public function processCampaign(int $campaignId): array
    {
        $campaignModel = new Campaign();
        $templateModel = new Template();
        $messageModel = new Message();
        $twilio = new TwilioService();
        $mailer = new EmailService();
        $queueJobModel = new QueueJob();
        $summary = ['campaigns' => 1, 'messages' => 0, 'failures' => 0, 'suppressed' => 0, 'deferred' => 0];
        $campaign = $campaignModel->find($campaignId);
        if (!$campaign) {
            throw new RuntimeException('Campaign not found.');
        }
        if (($campaign['approval_status'] ?? 'not_required') === 'pending') {
            return $summary;
        }
        if (!empty($campaign['scheduled_at']) && strtotime((string)$campaign['scheduled_at']) > time()) {
            return $summary;
        }
        $campaignModel->setStatus((int)$campaign['id'], 'processing');
        $template = $campaign['template_id'] ? $templateModel->find((int)$campaign['template_id']) : null;
        $deferredUntil = [];
        foreach ($campaignModel->queuedRecipients((int)$campaign['id'], 250) as $recipient) {
            try {
                $recipientTimezone = $recipient['timezone'] ?? quiet_hours_settings()['timezone'];
                if (is_within_quiet_hours($recipientTimezone)) {
                    $deferredUntil[] = next_allowed_send_at($recipientTimezone);
                    $summary['deferred']++;
                    continue;
                }
                $variant = $this->selectVariant($campaign, $recipient);
                $body = $this->bodyForVariant($campaign, $template, $recipient, $variant);
                if ($campaign['channel'] === 'sms') {
                    $body = apply_sms_preferences($body, (int)($campaign['created_by'] ?? 0));
                } else {
                    $body = apply_email_preferences($body, (int)($campaign['created_by'] ?? 0));
                }
                if ((new Suppression())->isSuppressed($campaign['channel'], $recipient['address'])) {
                    $campaignModel->markRecipient((int)$recipient['id'], 'failed', 'Suppressed recipient', null);
                    $summary['suppressed']++;
                    continue;
                }
                if ($campaign['channel'] === 'sms') {
                    $mediaUrl = !empty($template['media_path']) ? full_url('uploads/' . $template['media_path']) : null;
                    $result = $twilio->sendSms($recipient['address'], $body, $mediaUrl);
                    $messageId = $messageModel->create([
                        'campaign_id' => $campaign['id'],
                        'contact_id' => $recipient['contact_id'],
                        'channel' => 'sms',
                        'direction' => 'outbound',
                        'body' => $body,
                        'from_address' => effective_twilio_config((int)($campaign['created_by'] ?? 0))['from_number'],
                        'to_address' => $recipient['address'],
                        'status' => $result['status'] ?? 'sent',
                        'external_id' => $result['sid'] ?? null,
                        'media_url' => $mediaUrl,
                        'metadata_json' => json_encode(['variant' => $variant, 'provider' => $result]),
                    ]);
                } else {
                    $attachments = !empty($template['media_path']) ? [uploaded_file_path($template['media_path'])] : [];
                    $subject = $this->subjectForVariant($campaign, $template, $recipient, $variant);
                    $messageId = $messageModel->create([
                        'campaign_id' => $campaign['id'],
                        'contact_id' => $recipient['contact_id'],
                        'channel' => 'email',
                        'direction' => 'outbound',
                        'subject' => $subject,
                        'body' => $body,
                        'from_address' => effective_mail_config((int)($campaign['created_by'] ?? 0))['from_email'],
                        'to_address' => $recipient['address'],
                        'status' => 'queued',
                        'metadata_json' => json_encode(['variant' => $variant]),
                    ]);
                    $trackedHtml = instrument_email_html($body, $messageId);
                    $messageExternalId = '<msg-' . $messageId . '@' . parse_url(config('app.base_url'), PHP_URL_HOST) . '>';
                    $mailer->sendEmail($recipient['address'], $subject, $trackedHtml, strip_tags($body), $attachments, ['Message-ID: ' . $messageExternalId]);
                    $messageModel->updateExternalAndStatus($messageId, $messageExternalId, 'sent', null);
                }
                $campaignModel->markRecipient((int)$recipient['id'], 'sent', null, $messageId);
                $summary['messages']++;
            } catch (Throwable $e) {
                $campaignModel->markRecipient((int)$recipient['id'], 'failed', $e->getMessage(), null);
                $summary['failures']++;
            }
        }
        if ($campaignModel->hasQueuedRecipients((int)$campaign['id'])) {
            if ($deferredUntil) {
                sort($deferredUntil);
                $nextRun = $deferredUntil[0];
                $queueJobModel->create('campaign_process', ['campaign_id' => (int)$campaign['id']], $nextRun);
                $campaignModel->setStatus((int)$campaign['id'], 'queued');
            } else {
                $campaignModel->setStatus((int)$campaign['id'], 'processing');
            }
        } else {
            $campaignModel->setStatus((int)$campaign['id'], 'sent');
            $campaignModel->markRunCompleted((int)$campaign['id']);
            $this->scheduleNextRecurringCampaign($campaign, $campaignModel);
        }
        audit_log('campaign.processed', 'campaign', (int)$campaign['id'], $summary);
        return $summary;
    }

    public function processDueCampaigns(): array
    {
        $summary = ['campaigns' => 0, 'messages' => 0, 'failures' => 0];
        foreach ((new Campaign())->due() as $campaign) {
            $run = $this->processCampaign((int)$campaign['id']);
            $summary['campaigns'] += $run['campaigns'];
            $summary['messages'] += $run['messages'];
            $summary['failures'] += $run['failures'];
        }
        return $summary;
    }
}
