<?php

declare(strict_types=1);

namespace PpPhp\Modules\Settings\Controllers;

use PpPhp\Core\Config;
use PpPhp\Core\Email;
use PpPhp\Core\ModuleManager;
use PpPhp\Core\PlatformSettings;
use PpPhp\Core\Security;
use PpPhp\Core\Version;

/**
 * Core engine settings — superadmin only.
 *
 * These configure how PPphp ITSELF reaches the outside world (account emails,
 * one-time codes, and later SMS). They are entirely separate from a project's
 * Misc → Email/SMS settings and never reach the build engine or generated apps.
 */
class SettingsController
{
    public function __construct(
        private Config $config,
        private ModuleManager $moduleManager,
    ) {}

    public function index(): string
    {
        Security::RequireAuth();
        Security::RequireRoleOrAlert(
            ['superadmin'],
            'Settings are available to administrators only.'
        );

        $email     = PlatformSettings::get(PlatformSettings::EMAIL);
        $sms       = PlatformSettings::get(PlatformSettings::SMS);
        $templates = PlatformSettings::templates();
        $presets   = Email::presets();
        $placeholders = PlatformSettings::placeholders();
        $emailReady   = PlatformSettings::emailReady();
        $queuePending = \PpPhp\Core\MailQueue::pending();
        $queueFailed  = \PpPhp\Core\MailQueue::failedCount();

        $menu      = $this->moduleManager->collectMenu();
        $baseUrl   = $this->config->get('base_url');
        $assetsUrl = $this->config->get('assets_url');
        $user      = Security::CurrentUserData();
        $activeTab = in_array(($_GET['tab'] ?? ''), ['email', 'templates', 'sms'], true) ? $_GET['tab'] : 'email';

        ob_start();
        include __DIR__ . '/../Views/index.php';
        return ob_get_clean();
    }

    public function save(): void
    {
        header('Content-Type: application/json');
        Security::RequireAuth();
        if (!Security::IsAdmin()) {
            echo json_encode(['success' => false, 'message' => 'Settings are available to administrators only.']);
            exit;
        }
        \PpPhp\Core\Security::RequireCsrf();

        $input   = json_decode((string) file_get_contents('php://input'), true) ?: [];
        $section = (string) ($input['section'] ?? '');
        $data    = (array) ($input['data'] ?? []);

        switch ($section) {
            case 'email':
                $current = PlatformSettings::get(PlatformSettings::EMAIL);
                $save = [
                    'provider'    => (string) ($data['provider'] ?? 'custom'),
                    'host'        => trim((string) ($data['host'] ?? '')),
                    'port'        => (string) ($data['port'] ?? '587'),
                    'encryption'  => (string) ($data['encryption'] ?? 'tls'),
                    'username'    => trim((string) ($data['username'] ?? '')),
                    // A blank password means "keep the saved one" — the form never
                    // sends the stored password back to the browser.
                    'password'    => ($data['password'] ?? '') !== ''
                        ? (string) $data['password']
                        : (string) ($current['password'] ?? ''),
                    'fromAddress' => trim((string) ($data['fromAddress'] ?? '')),
                    'fromName'    => trim((string) ($data['fromName'] ?? '')),
                    'appName'     => trim((string) ($data['appName'] ?? '')) ?: 'PPphp',
                    'ownerEmail'  => trim((string) ($data['ownerEmail'] ?? '')),
                    // OAuth 2.0 ("sign in with Google/Microsoft"). authType flips the
                    // engine to AUTH XOAUTH2; the Client ID/Secret let us mint tokens.
                    // The refresh token + connected address are written by the OAuth
                    // CALLBACK (never the form), so they're preserved here. Secrets are
                    // stored encrypted.
                    'authType'      => in_array((string) ($data['authType'] ?? 'basic'), ['basic', 'xoauth2'], true) ? (string) $data['authType'] : 'basic',
                    'oauthProvider' => (string) ($data['oauthProvider'] ?? ($current['oauthProvider'] ?? '')),
                    'oauthClientId' => trim((string) ($data['oauthClientId'] ?? '')),
                    'oauthClientSecret' => ($data['oauthClientSecret'] ?? '') !== ''
                        ? PlatformSettings::secretEncrypt((string) $data['oauthClientSecret'])
                        : (string) ($current['oauthClientSecret'] ?? ''),
                    'oauthRefreshToken' => (string) ($current['oauthRefreshToken'] ?? ''),
                    'oauthEmail'        => (string) ($current['oauthEmail'] ?? ''),
                    // "Verified" = tested OK; the UI locks a verified card. The client
                    // sends false when the card was edited (so it must be re-tested).
                    'verified'          => (bool) ($data['verified'] ?? false),
                ];
                // Switching a mailbox back to password auth clears the stored OAuth grant.
                if ($save['authType'] !== 'xoauth2') {
                    $save['oauthRefreshToken'] = '';
                    $save['oauthEmail'] = '';
                }
                // Up to two fallback servers. If the primary can't deliver, the engine
                // tries these in order. Each keeps its saved password when the field is
                // left blank (the form never echoes stored passwords back).
                $curFb = (array) ($current['fallbacks'] ?? []);
                $save['fallbacks'] = [];
                // Slots are position-stable (0/1). A slot is kept if it has an SMTP host
                // OR an OAuth grant; otherwise it's blank/disabled and dropped. Secrets
                // (password, OAuth secret) and the OAuth grant keep their saved value when
                // the form field is blank. Each fallback can be app-password OR OAuth.
                foreach (array_slice((array) ($data['fallbacks'] ?? []), 0, 2) as $i => $fb) {
                    $fb       = (array) $fb;
                    $cur      = (array) ($curFb[$i] ?? []);
                    $host     = trim((string) ($fb['host'] ?? ''));
                    $authType = in_array((string) ($fb['authType'] ?? 'basic'), ['basic', 'xoauth2'], true) ? (string) $fb['authType'] : 'basic';
                    $refresh  = (string) ($cur['oauthRefreshToken'] ?? '');
                    $fbClientId = trim((string) ($fb['oauthClientId'] ?? ''));
                    // Keep the slot if it has an SMTP host, OR it's an OAuth slot with a
                    // client id entered (pending connect) or already connected.
                    if ($host === '' && !($authType === 'xoauth2' && ($fbClientId !== '' || $refresh !== ''))) continue;
                    $row = [
                        'host'        => $host,
                        'port'        => (string) ($fb['port'] ?? '587'),
                        'encryption'  => (string) ($fb['encryption'] ?? 'tls'),
                        'provider'    => (string) ($fb['provider'] ?? ($cur['provider'] ?? 'custom')),
                        'username'    => trim((string) ($fb['username'] ?? '')),
                        'password'    => ($fb['password'] ?? '') !== '' ? (string) $fb['password'] : (string) ($cur['password'] ?? ''),
                        'fromAddress' => trim((string) ($fb['fromAddress'] ?? '')),
                        'fromName'    => trim((string) ($fb['fromName'] ?? '')),
                        'authType'    => $authType,
                        'oauthProvider' => (string) ($fb['oauthProvider'] ?? ($cur['oauthProvider'] ?? '')),
                        'oauthClientId' => trim((string) ($fb['oauthClientId'] ?? '')),
                        'oauthClientSecret' => ($fb['oauthClientSecret'] ?? '') !== '' ? PlatformSettings::secretEncrypt((string) $fb['oauthClientSecret']) : (string) ($cur['oauthClientSecret'] ?? ''),
                        'oauthRefreshToken' => $authType === 'xoauth2' ? $refresh : '',
                        'oauthEmail'  => $authType === 'xoauth2' ? (string) ($cur['oauthEmail'] ?? '') : '',
                        'verified'    => (bool) ($fb['verified'] ?? false),
                    ];
                    $save['fallbacks'][] = $row;
                }
                $ok = PlatformSettings::set(PlatformSettings::EMAIL, $save);
                echo json_encode(['success' => $ok, 'message' => $ok ? 'Email settings saved.' : 'Could not save settings.']);
                exit;

            case 'templates':
                $clean = [];
                foreach (PlatformSettings::defaultTemplates() as $key => $def) {
                    $clean[$key] = [
                        'subject' => trim((string) ($data[$key]['subject'] ?? $def['subject'])),
                        'body'    => (string) ($data[$key]['body'] ?? $def['body']),
                        // On unless the switch was explicitly turned off.
                        'enabled' => !isset($data[$key]['enabled']) || (bool) $data[$key]['enabled'],
                    ];
                }
                $ok = PlatformSettings::set(PlatformSettings::TEMPLATES, $clean);
                echo json_encode(['success' => $ok, 'message' => $ok ? 'Email templates saved.' : 'Could not save templates.']);
                exit;

            case 'sms':
                $current = PlatformSettings::get(PlatformSettings::SMS);
                $save = [
                    'provider'  => (string) ($data['provider'] ?? ''),
                    'api_token' => ($data['api_token'] ?? '') !== ''
                        ? (string) $data['api_token']
                        : (string) ($current['api_token'] ?? ''),
                    'from'      => trim((string) ($data['from'] ?? '')),
                ];
                $ok = PlatformSettings::set(PlatformSettings::SMS, $save);
                echo json_encode(['success' => $ok, 'message' => $ok ? 'SMS settings saved.' : 'Could not save settings.']);
                exit;
        }

        echo json_encode(['success' => false, 'message' => 'Unknown settings section.']);
        exit;
    }

    /**
     * Switch a single template on/off independently — persists only the `enabled`
     * flag, so the admin's subject/body edits are left untouched.
     */
    public function toggleTemplate(): void
    {
        header('Content-Type: application/json');
        Security::RequireAuth();
        if (!Security::IsAdmin()) {
            echo json_encode(['success' => false, 'message' => 'Settings are available to administrators only.']);
            exit;
        }
        \PpPhp\Core\Security::RequireCsrf();

        $input   = json_decode((string) file_get_contents('php://input'), true) ?: [];
        $key     = (string) ($input['key'] ?? '');
        $enabled = (bool) ($input['enabled'] ?? false);
        if (!isset(PlatformSettings::defaultTemplates()[$key])) {
            echo json_encode(['success' => false, 'message' => 'Unknown template.']);
            exit;
        }

        $saved = PlatformSettings::get(PlatformSettings::TEMPLATES);
        $row = is_array($saved[$key] ?? null) ? $saved[$key] : [];
        $row['enabled'] = $enabled;      // preserve any existing subject/body override
        $saved[$key] = $row;

        $ok = PlatformSettings::set(PlatformSettings::TEMPLATES, $saved);
        echo json_encode([
            'success' => $ok,
            'enabled' => $enabled,
            'message' => $ok
                ? ('“' . PlatformSettings::defaultTemplates()[$key]['label'] . '” ' . ($enabled ? 'enabled.' : 'disabled — it will no longer be sent.'))
                : 'Could not update the template.',
        ]);
        exit;
    }

    /**
     * Restore a single template to its shipped default content, independently of
     * the others. Drops the saved override so it tracks the built-in default, and
     * switches it back on. Returns the default subject/body so the UI updates live.
     */
    public function resetTemplate(): void
    {
        header('Content-Type: application/json');
        Security::RequireAuth();
        if (!Security::IsAdmin()) {
            echo json_encode(['success' => false, 'message' => 'Settings are available to administrators only.']);
            exit;
        }
        \PpPhp\Core\Security::RequireCsrf();

        $input = json_decode((string) file_get_contents('php://input'), true) ?: [];
        $key   = (string) ($input['key'] ?? '');
        $defaults = PlatformSettings::defaultTemplates();
        if (!isset($defaults[$key])) {
            echo json_encode(['success' => false, 'message' => 'Unknown template.']);
            exit;
        }

        $saved = PlatformSettings::get(PlatformSettings::TEMPLATES);
        unset($saved[$key]); // fall back to the built-in default (content + enabled)
        $ok = PlatformSettings::set(PlatformSettings::TEMPLATES, $saved);

        echo json_encode([
            'success' => $ok,
            'message' => $ok ? ('“' . $defaults[$key]['label'] . '” restored to its default.') : 'Could not reset the template.',
            'subject' => $defaults[$key]['subject'],
            'body'    => $defaults[$key]['body'],
        ]);
        exit;
    }

    /**
     * Owner-only: update the platform version string shown in the sidebar.
     */
    public function saveVersion(): void
    {
        header('Content-Type: application/json');
        Security::RequireAuth();
        if (!Security::IsOwner()) {
            echo json_encode(['success' => false, 'message' => 'Only the platform owner can change the version.']);
            exit;
        }
        \PpPhp\Core\Security::RequireCsrf();

        $input   = json_decode((string) file_get_contents('php://input'), true) ?: [];
        $version = trim((string) ($input['version'] ?? ''));
        // Accept 2–4 dotted numeric segments, e.g. 1.0, 1.2.3, 2.10.0.1.
        if (!preg_match('/^\d{1,4}(\.\d{1,4}){1,3}$/', $version)) {
            echo json_encode(['success' => false, 'message' => 'Use a number like 1.0, 1.2 or 2.1.3.']);
            exit;
        }

        // The version is now central (Version::PLATFORM), so editing it here bumps
        // the actual code constant (forward-only), which every surface then reads.
        $normalised = Version::normalise($version);
        if (Version::compare($normalised, Version::PLATFORM) <= 0) {
            echo json_encode([
                'success' => false,
                'message' => 'The platform is already at ' . Version::PLATFORM . '. The version only moves forward.',
            ]);
            exit;
        }

        $ok = Version::stampCode($normalised);
        PlatformSettings::setVersion($normalised);   // keep the legacy row in sync (unused for display)
        echo json_encode([
            'success' => $ok,
            'version' => $ok ? $normalised : Version::PLATFORM,
            'message' => $ok
                ? 'Version updated to ' . $normalised . ' everywhere.'
                : 'Could not update the version file — check that core/Version.php is writable.',
        ]);
        exit;
    }

    /**
     * Render a template exactly as it will be emailed, using sample data, so the
     * author can see the finished branded email before saving.
     */
    public function preview(string $key): void
    {
        Security::RequireAuth();
        if (!Security::IsAdmin()) {
            http_response_code(403);
            echo 'Settings are available to administrators only.';
            exit;
        }

        $mail = \PpPhp\Core\Mailer::render($key, [
            'username'     => 'ppradhan',
            'email'        => 'ppradhan@live.com',
            'role'         => 'developer',
            'otp'          => '482913',
            'reset_url'    => \PpPhp\Core\Mailer::url('/reset-password?token=preview'),
            'expires_in'   => \PpPhp\Core\PasswordReset::TTL_MINUTES . ' minutes',
            // Sample data for the owner-bound trial-access request email.
            'full_name'    => 'Prakash Pradhan',
            'organization' => 'PPRADHAN',
            'role_title'   => 'Full-stack developer',
            'use_case'     => 'I want to build an inventory tracker for our workshop, with a couple of linked tables and a dashboard.',
            'duration'     => '3 days',
            'request_id'   => '42',
            'requested_at' => date('j M Y, g:i a'),
            'review_url'   => \PpPhp\Core\Mailer::url('/users?temporary_request=42'),
        ]);

        if ($mail['html'] === '') {
            http_response_code(404);
            echo 'Unknown template.';
            exit;
        }

        // Rendered inside a sandboxed iframe on the Settings page.
        header('Content-Type: text/html; charset=UTF-8');
        header('X-Content-Type-Options: nosniff');
        echo $mail['html'];
        exit;
    }

    /** Send a real test email to prove the configuration works. */
    public function testEmail(): void
    {
        header('Content-Type: application/json');
        @set_time_limit(60);
        ini_set('display_errors', '0');
        Security::RequireAuth();
        if (!Security::IsAdmin()) {
            echo json_encode(['success' => false, 'message' => 'Settings are available to administrators only.']);
            exit;
        }
        \PpPhp\Core\Security::RequireCsrf();

        $input = json_decode((string) file_get_contents('php://input'), true) ?: [];
        $to    = trim((string) ($input['to'] ?? ''));
        if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
            echo json_encode(['success' => false, 'message' => 'Enter a valid address to send the test to.']);
            exit;
        }

        if (!PlatformSettings::primeMailer()) {
            echo json_encode(['success' => false, 'message' => 'Save your email server settings first (host and From address are required).']);
            exit;
        }

        $settings = PlatformSettings::get(PlatformSettings::EMAIL);
        $appName  = (string) ($settings['appName'] ?? 'PPphp');

        try {
            $result = Email::send([
                'to'      => $to,
                'subject' => "{$appName} test email",
                'body'    => "This is a test email from {$appName}.\n\nIf you're reading this, your email server is configured correctly and account emails will work.",
            ]);
        } catch (\Throwable $e) {
            echo json_encode(['success' => false, 'message' => 'Send failed: ' . $e->getMessage()]);
            exit;
        }

        $ok = (bool) ($result['mailed'] ?? false);
        // Report which server delivered — and, if failover kicked in, that the
        // primary was skipped — so the admin can see the resilience working.
        $via = '';
        if ($ok) {
            $n = (int) ($result['server'] ?? 1);
            $host = (string) ($result['serverHost'] ?? '');
            if ($n > 1 || !empty($result['failovers'])) {
                $via = " Delivered via server #{$n}" . ($host !== '' ? " ({$host})" : '') . " after the earlier server(s) failed.";
            } elseif ($host !== '') {
                $via = " Delivered via {$host}.";
            }
        }
        echo json_encode([
            'success' => $ok,
            'message' => $ok
                ? "Test email sent to {$to}. Check the inbox (and the spam folder)." . $via
                : ('Could not send: ' . (string) ($result['message'] ?? 'unknown error')),
        ]);
        exit;
    }

    /**
     * Drain the retry queue on demand (Settings → Email Server → "Process now"), or
     * re-queue dead-lettered mail with ?action=retry-failed. Admin + POST only.
     */
    public function processQueue(): void
    {
        header('Content-Type: application/json');
        @set_time_limit(120);
        Security::RequireAuth();
        if (!Security::IsAdmin()) {
            echo json_encode(['success' => false, 'message' => 'Settings are available to administrators only.']);
            exit;
        }
        \PpPhp\Core\Security::RequireCsrf();

        $input  = json_decode((string) file_get_contents('php://input'), true) ?: [];
        $requeued = 0;
        if ((string) ($input['action'] ?? '') === 'retry-failed') {
            $requeued = \PpPhp\Core\MailQueue::retryFailed();
        }

        $stats = \PpPhp\Core\MailQueue::process(500);
        $parts = [];
        if ($requeued > 0)         $parts[] = "re-queued {$requeued} failed";
        if ($stats['sent'] > 0)    $parts[] = "sent {$stats['sent']}";
        if ($stats['retried'] > 0) $parts[] = "{$stats['retried']} will retry later";
        if ($stats['failed'] > 0)  $parts[] = "{$stats['failed']} gave up";
        if ($stats['processed'] === 0 && $requeued === 0) $parts[] = 'nothing due right now';

        echo json_encode([
            'success' => true,
            'message' => 'Queue processed — ' . implode(', ', $parts) . '.',
            'pending' => \PpPhp\Core\MailQueue::pending(),
            'failed'  => \PpPhp\Core\MailQueue::failedCount(),
        ]);
        exit;
    }

    /** The OAuth redirect URI (must be registered in the Google/Microsoft app). */
    private function emailRedirectUri(): string
    {
        return rtrim((string) $this->config->get('base_url'), '/') . '/settings/email/oauth/callback';
    }

    /** Read the config for a mail slot (0 = primary, 1|2 = fallbacks) from the EMAIL bag. */
    private function mailSlot(array $email, int $slot): array
    {
        if ($slot <= 0) return $email;
        return (array) (($email['fallbacks'] ?? [])[$slot - 1] ?? []);
    }

    /** Write a mail slot back into the EMAIL bag and persist. */
    private function saveMailSlot(array $email, int $slot, array $cfg): void
    {
        if ($slot <= 0) {
            $email = array_merge($email, $cfg);
        } else {
            $fb = (array) ($email['fallbacks'] ?? []);
            $fb[$slot - 1] = array_merge((array) ($fb[$slot - 1] ?? []), $cfg);
            $email['fallbacks'] = array_values($fb);
        }
        PlatformSettings::set(PlatformSettings::EMAIL, $email);
    }

    /** Step 1 — send the owner to Google/Microsoft to authorise a mailbox (any slot). */
    public function emailOauthStart(): void
    {
        Security::RequireAuth();
        if (!Security::IsAdmin()) { redirect('/settings'); }

        $slot     = max(0, min(2, (int) ($_GET['slot'] ?? 0)));
        $email    = PlatformSettings::get(PlatformSettings::EMAIL);
        $cfg      = $this->mailSlot($email, $slot);
        $provider = (string) ($cfg['oauthProvider'] ?? '');
        $clientId = (string) ($cfg['oauthClientId'] ?? '');
        if (!\PpPhp\Core\Mail\MailOAuth::isProvider($provider) || $clientId === '') {
            redirect('/settings?tab=email&oauth=needsetup');
        }
        $_SESSION['_email_oauth'] = ['state' => bin2hex(random_bytes(8)), 'provider' => $provider, 'slot' => $slot];
        $url = \PpPhp\Core\Mail\MailOAuth::authorizeUrl($provider, $clientId, $this->emailRedirectUri(), $_SESSION['_email_oauth']['state']);
        header('Location: ' . $url);   // external consent URL
        exit;
    }

    /** Step 2 — provider redirects back with a code → store the refresh token on the slot. */
    public function emailOauthCallback(): void
    {
        Security::RequireAuth();
        if (!Security::IsAdmin()) { redirect('/settings'); }

        $pending = $_SESSION['_email_oauth'] ?? null;
        unset($_SESSION['_email_oauth']);
        $code  = (string) ($_GET['code'] ?? '');
        $state = (string) ($_GET['state'] ?? '');
        if (!$pending || $code === '' || $state !== ($pending['state'] ?? '')) {
            redirect('/settings?tab=email&oauth=error');
        }
        $provider = (string) $pending['provider'];
        $slot     = (int) ($pending['slot'] ?? 0);
        $email    = PlatformSettings::get(PlatformSettings::EMAIL);
        $cfg      = $this->mailSlot($email, $slot);
        $tokens   = \PpPhp\Core\Mail\MailOAuth::exchangeCode(
            $provider,
            (string) ($cfg['oauthClientId'] ?? ''),
            PlatformSettings::secretDecrypt((string) ($cfg['oauthClientSecret'] ?? '')),
            $code,
            $this->emailRedirectUri()
        );
        $refresh = (string) ($tokens['refresh_token'] ?? '');
        if ($refresh === '') {
            redirect('/settings?tab=email&oauth=norefresh');
        }
        $mailAddr = \PpPhp\Core\Mail\MailOAuth::emailFromIdToken((string) ($tokens['id_token'] ?? ''));
        $patch = [
            'authType'          => 'xoauth2',
            'oauthProvider'     => $provider,
            'oauthRefreshToken' => PlatformSettings::secretEncrypt($refresh),
            'verified'          => false,   // connected, but still needs a delivery test
        ];
        if ($mailAddr !== '') {
            $patch['oauthEmail'] = $mailAddr;
            if (trim((string) ($cfg['fromAddress'] ?? '')) === '') $patch['fromAddress'] = $mailAddr;
        }
        $this->saveMailSlot($email, $slot, $patch);
        redirect('/settings?tab=email&oauth=connected');
    }

    /**
     * Test ONE mail server slot (no failover) against a recipient. On success the slot
     * is marked verified (which the UI locks). POST { slot, to }. Admin only.
     */
    public function testServer(): void
    {
        header('Content-Type: application/json');
        @set_time_limit(60);
        Security::RequireAuth();
        if (!Security::IsAdmin()) {
            echo json_encode(['success' => false, 'message' => 'Settings are available to administrators only.']);
            exit;
        }
        \PpPhp\Core\Security::RequireCsrf();
        $input = json_decode((string) file_get_contents('php://input'), true) ?: [];
        $slot  = max(0, min(2, (int) ($input['slot'] ?? 0)));
        $to    = trim((string) ($input['to'] ?? ''));
        if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
            echo json_encode(['success' => false, 'message' => 'Enter a valid address to send the test to.']);
            exit;
        }

        $email = PlatformSettings::get(PlatformSettings::EMAIL);
        $cfg   = PlatformSettings::resolveMailServer($this->mailSlot($email, $slot), $email);
        if (trim((string) ($cfg['host'] ?? '')) === '') {
            echo json_encode(['success' => false, 'message' => 'This server is not configured yet — save it first.']);
            exit;
        }
        // Send via THIS server only (no fallback), so the result reflects this slot.
        Email::setSettings($cfg);
        Email::setFallbacks([]);
        $appName = (string) ($email['appName'] ?? 'PPphp');
        try {
            $result = Email::send([
                'to'      => $to,
                'subject' => "{$appName} — server test",
                'body'    => "This is a test from mail server #" . ($slot + 1) . " of {$appName}. If you received it, this server is verified.",
            ]);
        } catch (\Throwable $e) {
            echo json_encode(['success' => false, 'message' => 'Send failed: ' . $e->getMessage()]);
            exit;
        }
        $ok = (bool) ($result['mailed'] ?? false);
        if ($ok) {
            // Lock the slot: mark it verified (surgical persist).
            $this->saveMailSlot(PlatformSettings::get(PlatformSettings::EMAIL), $slot, ['verified' => true]);
        }
        echo json_encode([
            'success'  => $ok,
            'verified' => $ok,
            'slot'     => $slot,
            'message'  => $ok
                ? "Server #" . ($slot + 1) . " verified — test delivered to {$to}."
                : ('Server #' . ($slot + 1) . ' could not send: ' . (string) ($result['message'] ?? 'unknown error')),
        ]);
        exit;
    }
}
