<?php

declare(strict_types=1);

namespace PpPhp\Core;

/**
 * Email API – send emails via SMTP using app passwords or custom credentials.
 *
 * Provider presets built in: Gmail, Outlook/Hotmail, Yahoo, Custom SMTP.
 * All sending is done through raw PHP sockets – zero external dependencies.
 *
 * Usage:
 *   // 1. Configure settings once (saved in project.json by the Misc module)
 *   Email::setSettings([
 *       'provider'    => 'gmail',
 *       'host'        => 'smtp.gmail.com',
 *       'port'        => 587,
 *       'encryption'  => 'tls',
 *       'username'    => 'you@gmail.com',
 *       'password'    => 'your-16-char-app-password',   // generate at myaccount.google.com/apppasswords
 *       'fromAddress' => 'you@gmail.com',
 *       'fromName'    => 'Your App',
 *   ]);
 *
 *   // 2. Send
 *   $result = Email::send([
 *       'to'      => 'user@example.com',
 *       'subject' => 'Hello',
 *       'body'    => 'Plain text body',
 *   ]);
 *   if ($result['mailed']) { echo "OK"; } else { echo $result['message']; }
 */
class Email
{
    private static array $settings = [];
    private static array $fallbacks = [];

    // ── Provider presets ──────────────────────────────────────────────

    /**
     * Provider presets — current as of 2026. Each entry carries not just host/port/
     * encryption but the modern AUTH story: `auth` (app_password | api_key | password),
     * the right field labels, a plain-language help line, and a direct link to create
     * the credential. `warn` flags providers phasing out basic SMTP auth. `usernameFixed`
     * pins the username to a literal the provider mandates (e.g. SendGrid's "apikey").
     *
     * Why app passwords / API keys: Gmail retired "less secure app" passwords in 2022,
     * and Microsoft is retiring Basic Auth SMTP — so a normal mailbox password no longer
     * works. Transactional providers (SendGrid, Brevo, …) authenticate with an API key,
     * which is the most reliable path for an application to send mail.
     */
    public static function presets(): array
    {
        return [
            // ── Personal / business mailboxes (SMTP + app password) ──
            'gmail' => [
                'label' => 'Gmail', 'group' => 'Mailboxes', 'oauth' => 'google',
                'host' => 'smtp.gmail.com', 'port' => 587, 'encryption' => 'tls',
                'auth' => 'app_password',
                'usernameLabel' => 'Gmail address', 'usernameHint' => 'your full name@gmail.com',
                'passwordLabel' => 'App password',
                'help' => 'Your normal Google password will NOT work. Turn on 2-Step Verification, then create a 16-character App Password.',
                'docsUrl' => 'https://myaccount.google.com/apppasswords', 'docsLabel' => 'Create a Google App Password',
            ],
            'outlook' => [
                'label' => 'Outlook.com / Hotmail', 'group' => 'Mailboxes', 'oauth' => 'microsoft',
                'host' => 'smtp-mail.outlook.com', 'port' => 587, 'encryption' => 'tls',
                'auth' => 'app_password',
                'usernameLabel' => 'Outlook address', 'usernameHint' => 'your @outlook.com / @hotmail.com address',
                'passwordLabel' => 'App password',
                'help' => 'Personal Outlook accounts need an App Password (enable two-step verification first).',
                'docsUrl' => 'https://account.live.com/proofs/AppPassword', 'docsLabel' => 'Create an Outlook App Password',
                'warn' => 'Microsoft is retiring password-based SMTP. If sending fails, this account likely requires OAuth — consider a transactional provider (SendGrid, Brevo…) below.',
            ],
            'office365' => [
                'label' => 'Microsoft 365 (business)', 'group' => 'Mailboxes', 'oauth' => 'microsoft',
                'host' => 'smtp.office365.com', 'port' => 587, 'encryption' => 'tls',
                'auth' => 'password',
                'usernameLabel' => 'Work email', 'usernameHint' => 'your Microsoft 365 mailbox',
                'passwordLabel' => 'Password',
                'help' => 'Microsoft 365 mailboxes increasingly require OAuth for SMTP.',
                'docsUrl' => 'https://learn.microsoft.com/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission',
                'docsLabel' => 'Microsoft 365 SMTP guide',
                'warn' => 'Microsoft is retiring Basic Auth SMTP (new tenants already have it off). If this fails, use a transactional provider below.',
            ],
            'yahoo' => [
                'label' => 'Yahoo Mail', 'group' => 'Mailboxes',
                'host' => 'smtp.mail.yahoo.com', 'port' => 465, 'encryption' => 'ssl',
                'auth' => 'app_password',
                'usernameLabel' => 'Yahoo address', 'usernameHint' => 'your full @yahoo.com address',
                'passwordLabel' => 'App password',
                'help' => 'Yahoo rejects your normal password. Generate an App Password from Account Security.',
                'docsUrl' => 'https://login.yahoo.com/account/security/app-passwords', 'docsLabel' => 'Create a Yahoo App Password',
            ],
            'icloud' => [
                'label' => 'iCloud Mail', 'group' => 'Mailboxes',
                'host' => 'smtp.mail.me.com', 'port' => 587, 'encryption' => 'tls',
                'auth' => 'app_password',
                'usernameLabel' => 'iCloud address', 'usernameHint' => 'your @icloud.com address',
                'passwordLabel' => 'App-specific password',
                'help' => 'Apple requires an app-specific password (port 587 only — 465 is not supported).',
                'docsUrl' => 'https://appleid.apple.com', 'docsLabel' => 'Create an Apple app-specific password',
            ],
            'zoho' => [
                'label' => 'Zoho Mail', 'group' => 'Mailboxes',
                'host' => 'smtp.zoho.com', 'port' => 465, 'encryption' => 'ssl',
                'auth' => 'app_password',
                'usernameLabel' => 'Zoho address', 'usernameHint' => 'your Zoho mailbox',
                'passwordLabel' => 'App password',
                'help' => 'Generate an application-specific password in Zoho if two-factor auth is on.',
                'docsUrl' => 'https://www.zoho.com/mail/help/adminconsole/two-factor-authentication.html', 'docsLabel' => 'Zoho app passwords',
            ],

            // ── Transactional providers (SMTP + API key — best for apps) ──
            'sendgrid' => [
                'label' => 'SendGrid', 'group' => 'Transactional (API key)',
                'host' => 'smtp.sendgrid.net', 'port' => 587, 'encryption' => 'tls',
                'auth' => 'api_key', 'usernameFixed' => 'apikey',
                'usernameLabel' => 'Username', 'usernameHint' => 'always the literal word “apikey”',
                'passwordLabel' => 'API key',
                'help' => 'Username is literally “apikey”; the password is the API key you create in SendGrid.',
                'docsUrl' => 'https://app.sendgrid.com/settings/api_keys', 'docsLabel' => 'Create a SendGrid API key',
            ],
            'brevo' => [
                'label' => 'Brevo (Sendinblue)', 'group' => 'Transactional (API key)',
                'host' => 'smtp-relay.brevo.com', 'port' => 587, 'encryption' => 'tls',
                'auth' => 'api_key',
                'usernameLabel' => 'SMTP login', 'usernameHint' => 'your Brevo SMTP login (an email)',
                'passwordLabel' => 'SMTP key',
                'help' => 'Use the SMTP login + master SMTP key from Brevo → SMTP & API.',
                'docsUrl' => 'https://app.brevo.com/settings/keys/smtp', 'docsLabel' => 'Get Brevo SMTP key',
            ],
            'mailgun' => [
                'label' => 'Mailgun', 'group' => 'Transactional (API key)',
                'host' => 'smtp.mailgun.org', 'port' => 587, 'encryption' => 'tls',
                'auth' => 'api_key',
                'usernameLabel' => 'SMTP username', 'usernameHint' => 'postmaster@your-domain (from Mailgun)',
                'passwordLabel' => 'SMTP password',
                'help' => 'Use the per-domain SMTP credentials Mailgun shows for your sending domain.',
                'docsUrl' => 'https://app.mailgun.com/mg/sending/domains', 'docsLabel' => 'Mailgun SMTP credentials',
            ],
            'postmark' => [
                'label' => 'Postmark', 'group' => 'Transactional (API key)',
                'host' => 'smtp.postmarkapp.com', 'port' => 587, 'encryption' => 'tls',
                'auth' => 'api_key',
                'usernameLabel' => 'Server API token', 'usernameHint' => 'paste your Server API token',
                'passwordLabel' => 'Server API token',
                'help' => 'Postmark uses the SAME Server API Token for both username and password.',
                'docsUrl' => 'https://account.postmarkapp.com', 'docsLabel' => 'Postmark server tokens',
            ],
            'amazon_ses' => [
                'label' => 'Amazon SES', 'group' => 'Transactional (API key)',
                'host' => 'email-smtp.us-east-1.amazonaws.com', 'port' => 587, 'encryption' => 'tls',
                'auth' => 'api_key', 'hostEditable' => true,
                'usernameLabel' => 'SMTP username', 'usernameHint' => 'SES SMTP credential (NOT your AWS key)',
                'passwordLabel' => 'SMTP password',
                'help' => 'Change the host region to match yours (e.g. email-smtp.eu-west-1.amazonaws.com). Generate dedicated SMTP credentials in the SES console.',
                'docsUrl' => 'https://console.aws.amazon.com/ses/home#/smtp', 'docsLabel' => 'Create SES SMTP credentials',
            ],
            'resend' => [
                'label' => 'Resend', 'group' => 'Transactional (API key)',
                'host' => 'smtp.resend.com', 'port' => 587, 'encryption' => 'tls',
                'auth' => 'api_key', 'usernameFixed' => 'resend',
                'usernameLabel' => 'Username', 'usernameHint' => 'always the literal word “resend”',
                'passwordLabel' => 'API key',
                'help' => 'Username is literally “resend”; the password is your Resend API key.',
                'docsUrl' => 'https://resend.com/api-keys', 'docsLabel' => 'Create a Resend API key',
            ],
            'mailjet' => [
                'label' => 'Mailjet', 'group' => 'Transactional (API key)',
                'host' => 'in-v3.mailjet.com', 'port' => 587, 'encryption' => 'tls',
                'auth' => 'api_key',
                'usernameLabel' => 'API key', 'usernameHint' => 'your Mailjet API key',
                'passwordLabel' => 'Secret key',
                'help' => 'Username = API key, password = Secret key (Mailjet → API Key Management).',
                'docsUrl' => 'https://app.mailjet.com/account/apikeys', 'docsLabel' => 'Mailjet API keys',
            ],

            'custom' => [
                'label' => 'Custom SMTP', 'group' => 'Other',
                'host' => '', 'port' => 587, 'encryption' => 'tls',
                'auth' => 'password', 'hostEditable' => true,
                'usernameLabel' => 'Username', 'usernameHint' => 'usually your full email address',
                'passwordLabel' => 'Password',
                'help' => 'Enter your own SMTP server details. Port 465 = SSL/TLS, 587 = STARTTLS.',
                'docsUrl' => '', 'docsLabel' => '',
            ],
        ];
    }

    // ── Settings management ──────────────────────────────────────────

    public static function setSettings(array $settings): void
    {
        self::$settings = array_merge(self::$settings, $settings);
    }

    public static function getSettings(): array
    {
        return self::$settings;
    }

    /**
     * Ordered fallback servers, each a full server config (host/port/encryption/
     * username/password/fromAddress/fromName). If the primary can't deliver, send()
     * tries these in turn. Pass [] to clear. This is what makes delivery resilient
     * to one provider being unreachable, rate-limited or misconfigured.
     */
    public static function setFallbacks(array $servers): void
    {
        $clean = [];
        foreach ($servers as $srv) {
            if (is_array($srv) && trim((string) ($srv['host'] ?? '')) !== '') {
                $clean[] = $srv;
            }
        }
        self::$fallbacks = $clean;
    }

    // ── Send ─────────────────────────────────────────────────────────

    /**
     * Send an email.
     *
     * @param array $params  Required: to, subject, body|htmlbody.
     *                       Optional: cc, bcc, replyTo, from, fromName, priority, attachments.
     * @return array ['mailed' => bool, 'message' => string]
     */
    /**
     * Send, with automatic failover. Tries the primary server, then each configured
     * fallback in order; the FIRST server that accepts the message wins. Only a
     * genuine delivery failure (unreachable host, auth/TLS error, server refusal)
     * advances to the next server — a hard rejection is still returned. The result
     * adds: server (1-based index that delivered), serverHost, and, when earlier
     * servers were skipped, failovers[] describing why.
     *
     * @return array ['mailed' => bool, 'message' => string, ...]
     */
    public static function send(array $params): array
    {
        // Validate once, up front — a missing subject/recipient is not something a
        // second server would fix, so don't burn the whole chain on it.
        $to = $params['to'] ?? '';
        if (empty($to) || empty($params['subject'] ?? '') || (empty($params['body'] ?? '') && empty($params['htmlbody'] ?? ''))) {
            return ['mailed' => false, 'message' => 'Missing required fields (to, subject, body/htmlbody).'];
        }

        // Build the ordered delivery chain: primary first, then fallbacks.
        $servers = [];
        if (trim((string) (self::$settings['host'] ?? '')) !== '') $servers[] = self::$settings;
        foreach (self::$fallbacks as $fb) $servers[] = $fb;

        if ($servers === []) {
            return ['mailed' => false, 'message' => 'No email server is configured.'];
        }

        $errors = [];
        foreach ($servers as $i => $cfg) {
            $res = self::attempt($cfg, $params);
            if (!empty($res['mailed'])) {
                $res['server']     = $i + 1;
                $res['serverHost'] = (string) ($cfg['host'] ?? '');
                if ($errors !== []) $res['failovers'] = $errors;   // which servers we skipped past
                return $res;
            }
            $errors[] = 'Server ' . ($i + 1) . ' (' . ((string) ($cfg['host'] ?? '?')) . '): ' . $res['message'];
        }

        return [
            'mailed'   => false,
            'message'  => count($servers) === 1
                ? $errors[0]
                : 'All ' . count($servers) . ' email servers failed — ' . implode(' | ', $errors),
            'attempts' => $errors,
        ];
    }

    /** Deliver via ONE server config. Returns ['mailed'=>bool,'message'=>string]. */
    private static function attempt(array $s, array $params): array
    {
        $to       = $params['to'] ?? '';
        $subject  = $params['subject'] ?? '';
        $body     = $params['body'] ?? '';
        $htmlBody = $params['htmlbody'] ?? '';
        $from     = $params['from'] ?? $s['fromAddress'] ?? '';
        $fromName = $params['fromName'] ?? $s['fromName'] ?? '';
        $cc       = $params['cc'] ?? '';
        $bcc      = $params['bcc'] ?? '';
        $replyTo  = $params['replyTo'] ?? '';
        $priority = (int)($params['priority'] ?? 3);
        $attachments = $params['attachments'] ?? [];

        if (empty($to) || empty($subject) || (empty($body) && empty($htmlBody))) {
            return ['mailed' => false, 'message' => 'Missing required fields (to, subject, body/htmlbody).'];
        }
        if (empty($s['host'])) {
            return ['mailed' => false, 'message' => 'SMTP host is not configured.'];
        }

        try {
            $socket = self::connect($s);
            self::ehlo($socket, $s);

            // STARTTLS is for explicit-TLS on a plaintext port (e.g. 587): connect in
            // the clear, then upgrade. On an implicit-TLS port (465 / encryption 'ssl')
            // the TLS handshake already happened at connect, so issuing STARTTLS there
            // is an error — skip it.
            if (self::useStartTls($s)) {
                self::command($socket, "STARTTLS");
                if (!@stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
                    throw new \RuntimeException('Could not start TLS (STARTTLS handshake failed) — the server may not support STARTTLS on this port. Try port 465 with SSL/TLS.');
                }
                self::ehlo($socket, $s);
            }

            if (!empty($s['username'])) {
                // XOAUTH2 (OAuth 2.0 bearer token) when the config carries one — the
                // modern, token-based path Google/Microsoft are moving to. Falls back
                // to AUTH LOGIN (app password / API key) otherwise.
                $authType = strtolower(trim((string) ($s['authType'] ?? '')));
                if ($authType === 'xoauth2' && !empty($s['oauthToken'])) {
                    $xo = base64_encode("user=" . $s['username'] . "\x01auth=Bearer " . $s['oauthToken'] . "\x01\x01");
                    self::command($socket, "AUTH XOAUTH2 " . $xo);
                } else {
                    self::command($socket, "AUTH LOGIN");
                    self::command($socket, base64_encode($s['username']));
                    self::command($socket, base64_encode($s['password'] ?? ''));
                }
            }

            self::command($socket, "MAIL FROM:<{$from}>");
            foreach (array_unique(self::extractEmails($to . ',' . $cc . ',' . $bcc)) as $addr) {
                self::command($socket, "RCPT TO:<{$addr}>");
            }

            self::command($socket, "DATA");
            $message = self::buildMessage($params, $from, $fromName, $replyTo, $priority, $attachments);
            self::command($socket, $message . "\r\n.");
            self::command($socket, "QUIT");
            fclose($socket);

            return ['mailed' => true, 'message' => 'OK'];
        } catch (\Throwable $e) {
            // \Throwable (not just \Exception) so a TypeError/Error inside the SMTP
            // dialogue also returns a clean result instead of a raw fatal.
            if (isset($socket) && is_resource($socket)) { @fclose($socket); }
            return ['mailed' => false, 'message' => $e->getMessage()];
        }
    }

    // ── Private helpers ──────────────────────────────────────────────

    /** Implicit TLS (SMTPS): TLS is negotiated at connect. Port 465, or encryption 'ssl'. */
    private static function isImplicitTls(array $s): bool
    {
        $enc  = strtolower(trim((string)($s['encryption'] ?? '')));
        $port = (int)($s['port'] ?? 587);
        return $enc === 'ssl' || $port === 465;
    }

    /** STARTTLS upgrade after a plaintext connect (port 587). Not on an implicit-TLS port. */
    private static function useStartTls(array $s): bool
    {
        $enc = strtolower(trim((string)($s['encryption'] ?? '')));
        return !self::isImplicitTls($s) && in_array($enc, ['tls', 'starttls'], true);
    }

    /**
     * Read a full SMTP reply (handles multi-line "250-…\r\n250 …"). Always returns a
     * string — never false — so callers can substr() the status code safely.
     */
    private static function readReply($socket): string
    {
        $data = '';
        while (($line = fgets($socket, 515)) !== false) {
            $data .= $line;
            // A space at index 3 ("250 …") marks the final line; a hyphen ("250-…")
            // means more lines follow. Short lines are treated as final.
            if (strlen($line) < 4 || substr($line, 3, 1) === ' ') break;
        }
        return $data;
    }

    private static function connect(array $s)
    {
        $host   = (string)($s['host'] ?? '');
        $port   = (int)($s['port'] ?? 587);
        // Choose the transport from the port/encryption, not just the label: a server
        // on 465 speaks TLS immediately, so we must connect with ssl:// or the greeting
        // never arrives (fgets returns false → the old substr() crash).
        $target = self::isImplicitTls($s) ? 'ssl://' . $host : $host;
        $socket = @fsockopen($target, $port, $errno, $errstr, 15);
        if (!$socket) {
            throw new \RuntimeException("Could not connect to {$host}:{$port} — "
                . ($errstr !== '' ? $errstr : 'connection failed') . ($errno ? " (#{$errno})" : '')
                . '. Check host/port/encryption — port 465 = SSL/TLS, port 587 = STARTTLS.');
        }
        stream_set_timeout($socket, 15);
        $greeting = self::readReply($socket);
        if (strncmp($greeting, '220', 3) !== 0) {
            @fclose($socket);
            $hint = trim($greeting) === ''
                ? ' (no reply — the port/encryption combination is likely wrong: try 465 with SSL/TLS, or 587 with STARTTLS).'
                : '';
            throw new \RuntimeException('Unexpected SMTP greeting: '
                . (trim($greeting) !== '' ? trim($greeting) : '(empty)') . $hint);
        }
        return $socket;
    }

    private static function ehlo($socket, array $s): void
    {
        self::command($socket, "EHLO " . ($s['host'] ?? 'localhost'));
    }

    private static function command($socket, string $cmd): void
    {
        fwrite($socket, $cmd . "\r\n");
        $response = self::readReply($socket);        // always a string
        $code = substr($response, 0, 1);
        if ($code === '4' || $code === '5') {
            // Show only the server's own reply — never echo $cmd, which for the AUTH
            // steps would leak the base64 username/password.
            throw new \RuntimeException('SMTP server rejected the request: '
                . (trim($response) !== '' ? trim($response) : 'error code ' . $code . 'xx (no message).'));
        }
    }

    private static function extractEmails(string $list): array
    {
        $emails = [];
        foreach (explode(',', $list) as $addr) {
            $addr = trim($addr);
            if (filter_var($addr, FILTER_VALIDATE_EMAIL)) {
                $emails[] = $addr;
            }
        }
        return $emails;
    }

    private static function buildMessage(
        array $params, string $from, string $fromName, string $replyTo,
        int $priority, array $attachments
    ): string {
        $to       = $params['to'] ?? '';
        $subject  = $params['subject'] ?? '';
        $body     = $params['body'] ?? '';
        $htmlBody = $params['htmlbody'] ?? '';
        $cc       = $params['cc'] ?? '';
        $bcc      = $params['bcc'] ?? '';

        $headers = "From: " . (empty($fromName) ? $from : "$fromName <$from>") . "\r\n";
        $headers .= "To: $to\r\n";
        if ($cc) $headers .= "Cc: $cc\r\n";
        if ($bcc) $headers .= "Bcc: $bcc\r\n";
        if ($replyTo) $headers .= "Reply-To: $replyTo\r\n";
        $headers .= "X-Priority: $priority\r\n";
        $headers .= "MIME-Version: 1.0\r\n";

        if (!empty($htmlBody)) {
            $headers .= "Content-Type: text/html; charset=UTF-8\r\n";
            $content = $htmlBody;
        } else {
            $headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
            $content = $body;
        }

        return "Subject: =?UTF-8?B?" . base64_encode($subject) . "=?=\r\n" . $headers . "\r\n" . $content;
    }
}