<?php
/**
 * Outbox — unified queue for SMS (Beem) and email (SMTP).
 *
 * Producers (alert emitter, expense approver, trip dispatcher, etc.) call
 * queueSms() / queueEmail(). Consumer is cron/tick.php which calls
 * dispatchPendingSms() / dispatchPendingEmail() every 5 minutes.
 *
 * Retry policy: 3 attempts. On the 3rd failure, status flips to 'failed'
 * and won't be retried automatically. Ops can requeue via
 * `UPDATE ... SET status='queued', attempts=0 WHERE c0d=?`.
 */
class Outbox extends Model
{
    private const MAX_PER_TICK = 30;         // hard cap on messages sent per cron tick

    public function queueSms(string $msisdn, string $message, array $meta = []): int
    {
        $msisdn = self::normaliseMsisdn($msisdn);
        if ($msisdn === '' || $message === '') return 0;

        $r = (new DB())->insertUpdate(
            "INSERT INTO sms_outbox
                (tenant_id, msisdn, message, alert_id, source_type, source_id,
                 status, attempts, max_attempts, scheduled_for, created_by, created_on)
             VALUES
                (:tenant, :m, :msg, :aid, :st, :sid,
                 'queued', 0, 3, :sched, :uid, NOW())",
            [
                ':tenant' => defined('DEFAULT_TENANT_ID') ? DEFAULT_TENANT_ID : 1,
                ':m'      => $msisdn,
                ':msg'    => mb_substr($message, 0, 480),
                ':aid'    => $meta['alert_id']    ?? null,
                ':st'     => $meta['source_type'] ?? null,
                ':sid'    => $meta['source_id']   ?? null,
                ':sched'  => $meta['scheduled_for'] ?? null,
                ':uid'    => $meta['user_id']     ?? null,
            ]
        );
        return (int)$r['last_id'];
    }

    public function queueEmail(string $to, string $subject, string $bodyHtml, array $meta = []): int
    {
        if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) return 0;

        $r = (new DB())->insertUpdate(
            "INSERT INTO email_outbox
                (tenant_id, to_email, to_name, cc_emails, subject, body_html, body_text,
                 alert_id, source_type, source_id, status, attempts, max_attempts,
                 scheduled_for, created_by, created_on)
             VALUES
                (:tenant, :to, :tn, :cc, :sub, :html, :text,
                 :aid, :st, :sid, 'queued', 0, 3,
                 :sched, :uid, NOW())",
            [
                ':tenant' => defined('DEFAULT_TENANT_ID') ? DEFAULT_TENANT_ID : 1,
                ':to'     => $to,
                ':tn'     => $meta['to_name']   ?? null,
                ':cc'     => $meta['cc_emails'] ?? null,
                ':sub'    => mb_substr($subject, 0, 250),
                ':html'   => $bodyHtml,
                ':text'   => $meta['body_text'] ?? strip_tags($bodyHtml),
                ':aid'    => $meta['alert_id']    ?? null,
                ':st'     => $meta['source_type'] ?? null,
                ':sid'    => $meta['source_id']   ?? null,
                ':sched'  => $meta['scheduled_for'] ?? null,
                ':uid'    => $meta['user_id']     ?? null,
            ]
        );
        return (int)$r['last_id'];
    }

    /**
     * Called from cron/tick.php. Iterates pending SMS rows, calls Beem, updates state.
     * Returns [sent, failed] counts.
     */
    public function dispatchPendingSms(int $wallClockBudgetSec = 25): array
    {
        $db = new DB();
        $rows = $db->mySelect(
            "SELECT c0d, msisdn, message, alert_id, attempts, max_attempts
               FROM sms_outbox
              WHERE status = 'queued'
                AND (scheduled_for IS NULL OR scheduled_for <= NOW())
                AND attempts < max_attempts
           ORDER BY created_on ASC
              LIMIT " . self::MAX_PER_TICK,
            []
        ) ?: [];

        $sent = 0; $failed = 0; $started = microtime(true);
        $beem = new BeemSms();
        foreach ($rows as $row) {
            if ((microtime(true) - $started) > $wallClockBudgetSec) break;

            $db->insertUpdate("UPDATE sms_outbox SET status='sending', attempts = attempts + 1 WHERE c0d = :id", [':id' => $row['c0d']]);
            $r = $beem->send($row['msisdn'], $row['message']);
            if ($r['ok']) {
                $db->insertUpdate("UPDATE sms_outbox SET status='sent', sent_at=NOW(), provider_msg_id = :pid, last_error = NULL WHERE c0d = :id",
                    [':pid' => $r['message_id'] ?? null, ':id' => $row['c0d']]);
                if ($row['alert_id']) $db->insertUpdate("UPDATE alerts SET sms_sent = 1 WHERE c0d = :id", [':id' => $row['alert_id']]);
                $sent++;
            } else {
                $finalFail = ((int)$row['attempts'] + 1) >= (int)$row['max_attempts'];
                $db->insertUpdate(
                    "UPDATE sms_outbox SET status = :st, last_error = :err WHERE c0d = :id",
                    [':st' => $finalFail ? 'failed' : 'queued', ':err' => mb_substr((string)($r['msg'] ?? 'unknown'), 0, 480), ':id' => $row['c0d']]
                );
                $failed++;
            }
        }
        return ['sent' => $sent, 'failed' => $failed];
    }

    public function dispatchPendingEmail(int $wallClockBudgetSec = 25): array
    {
        $db = new DB();
        $rows = $db->mySelect(
            "SELECT c0d, to_email, to_name, cc_emails, subject, body_html, body_text, alert_id, attempts, max_attempts
               FROM email_outbox
              WHERE status = 'queued'
                AND (scheduled_for IS NULL OR scheduled_for <= NOW())
                AND attempts < max_attempts
           ORDER BY created_on ASC
              LIMIT " . self::MAX_PER_TICK,
            []
        ) ?: [];

        $sent = 0; $failed = 0; $started = microtime(true);
        foreach ($rows as $row) {
            if ((microtime(true) - $started) > $wallClockBudgetSec) break;

            $db->insertUpdate("UPDATE email_outbox SET status='sending', attempts = attempts + 1 WHERE c0d = :id", [':id' => $row['c0d']]);
            $r = self::sendEmail($row);
            if ($r['ok']) {
                $db->insertUpdate("UPDATE email_outbox SET status='sent', sent_at=NOW(), last_error=NULL WHERE c0d = :id", [':id' => $row['c0d']]);
                if ($row['alert_id']) $db->insertUpdate("UPDATE alerts SET email_sent = 1 WHERE c0d = :id", [':id' => $row['alert_id']]);
                $sent++;
            } else {
                $finalFail = ((int)$row['attempts'] + 1) >= (int)$row['max_attempts'];
                $db->insertUpdate(
                    "UPDATE email_outbox SET status = :st, last_error = :err WHERE c0d = :id",
                    [':st' => $finalFail ? 'failed' : 'queued', ':err' => mb_substr((string)($r['msg'] ?? 'unknown'), 0, 480), ':id' => $row['c0d']]
                );
                $failed++;
            }
        }
        return ['sent' => $sent, 'failed' => $failed];
    }

    /**
     * Send one email via SMTP (or PHP mail() fallback if SMTP_HOST not configured).
     * SMTP path uses raw socket — no PHPMailer dependency to keep the tree lean.
     */
    private static function sendEmail(array $row): array
    {
        $to      = $row['to_email'];
        $subject = $row['subject'];
        $html    = $row['body_html'];
        $from    = defined('MAIL_FROM')      ? MAIL_FROM      : 'no-reply@localhost';
        $fromName= defined('MAIL_FROM_NAME') ? MAIL_FROM_NAME : 'Pacific Logistics';

        // No SMTP configured → fallback to PHP mail()
        if (!defined('SMTP_HOST') || SMTP_HOST === '') {
            $headers = "MIME-Version: 1.0\r\n"
                     . "Content-Type: text/html; charset=UTF-8\r\n"
                     . "From: {$fromName} <{$from}>\r\n";
            $ok = @mail($to, $subject, $html, $headers);
            return $ok ? ['ok' => true] : ['ok' => false, 'msg' => 'mail() returned false'];
        }

        // Minimal SMTP client
        $host = SMTP_HOST;
        $port = defined('SMTP_PORT') ? (int)SMTP_PORT : 587;
        $secure = defined('SMTP_SECURE') ? SMTP_SECURE : 'tls';
        $user = defined('SMTP_USER') ? SMTP_USER : '';
        $pass = defined('SMTP_PASS') ? SMTP_PASS : '';

        $errno = 0; $errstr = '';
        $connectHost = $secure === 'ssl' ? "ssl://{$host}" : $host;
        $sock = @stream_socket_client("{$connectHost}:{$port}", $errno, $errstr, 20);
        if (!$sock) return ['ok' => false, 'msg' => "SMTP connect: {$errstr}"];

        $read = function() use ($sock) { $r = ''; while (($l = fgets($sock, 515)) !== false) { $r .= $l; if (isset($l[3]) && $l[3] === ' ') break; } return $r; };
        $cmd  = function($c) use ($sock, $read) { fwrite($sock, $c . "\r\n"); return $read(); };

        $read();
        $cmd("EHLO pacific.local");
        if ($secure === 'tls') {
            $cmd("STARTTLS");
            stream_socket_enable_crypto($sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
            $cmd("EHLO pacific.local");
        }
        if ($user !== '') {
            $cmd("AUTH LOGIN");
            $cmd(base64_encode($user));
            $r = $cmd(base64_encode($pass));
            if (strpos($r, '235') !== 0) { fclose($sock); return ['ok'=>false, 'msg' => "SMTP auth: " . trim($r)]; }
        }
        $cmd("MAIL FROM:<{$from}>");
        $cmd("RCPT TO:<{$to}>");
        $r = $cmd("DATA");
        if (strpos($r, '354') !== 0) { fclose($sock); return ['ok'=>false, 'msg' => "SMTP DATA: " . trim($r)]; }

        $body  = "From: {$fromName} <{$from}>\r\n";
        $body .= "To: {$to}\r\n";
        $body .= "Subject: {$subject}\r\n";
        $body .= "MIME-Version: 1.0\r\n";
        $body .= "Content-Type: text/html; charset=UTF-8\r\n";
        $body .= "\r\n" . str_replace("\r\n.", "\r\n..", $html) . "\r\n.\r\n";
        $r = $cmd($body);
        $cmd("QUIT");
        fclose($sock);
        return strpos($r, '250') === 0 ? ['ok' => true] : ['ok' => false, 'msg' => "SMTP send: " . trim($r)];
    }

    /**
     * Convert local msisdn variants (0712345678, 255712345678) to E.164 (+255712345678).
     * Tanzania-first heuristic; leave anything already starting with + alone.
     */
    public static function normaliseMsisdn(string $m): string
    {
        $m = preg_replace('/[^\d+]/', '', trim($m));
        if ($m === '') return '';
        if ($m[0] === '+') return $m;
        if (strlen($m) === 10 && $m[0] === '0') return '+255' . substr($m, 1);
        if (strlen($m) === 9)                    return '+255' . $m;
        if (strlen($m) === 12 && strpos($m, '255') === 0) return '+' . $m;
        return '+' . $m;
    }
}
