<?php
/**
 * MAFIA SINGLE SENDS ROTATION
 *
 * Professional Email Marketing System with Installation Wizard
 * 
 * Features:
 * - Database Installation Wizard
 * - Admin Authentication System
 * - Multi-profile rotation sending
 * - Real-time campaign progress tracking
 * - Contact list management
 * - Email tracking (opens, clicks, bounces)
 * 
 * Enhanced with:
 * - First-time setup wizard with professional UI
 * - Secure admin login
 * - Automatic database table creation
 * - Fixed campaign status bug (campaigns no longer stuck at "sending")
 * 
 * HIGH-LOAD OPTIMIZATION (64GB RAM, 16 vCPUs Server):
 * ================================================
 * 
 * CONNECTION MANAGEMENT:
 * - Operation locks prevent simultaneous fetch/send (file-based mutex)
 * - Each worker creates LOCAL database connection (never passed)
 * - Explicit connection closing with $pdo = null after use
 * - Short timeouts: 20s operation, 20s session (wait_timeout/interactive_timeout)
 * - Non-persistent connections to prevent accumulation
 * - Maximum 160 concurrent workers (MAX_WORKERS) to prevent DB exhaustion
 * 
 * SENDGRID-STYLE BATCH PROCESSING:
 * - Recipients pre-loaded into memory before worker spawn
 * - In-memory distribution across workers (no per-email DB queries)
 * - Workers receive batches via temp files (RAM disk /dev/shm preferred)
 * - Batch spawning with delays to prevent connection spikes
 * - Supports 1M+ emails per minute with proper resource allocation
 * 
 * OPERATION SEPARATION:
 * - Send operations hold 'send' lock during execution
 * - Fetch operations (IMAP bounce scan) hold 'fetch' lock
 * - Locks prevent concurrent fetch/send as per requirements
 * - Lock files stored in temp directory with timeout support
 */

///////////////////////
//  LOAD CONFIGURATION
///////////////////////
// Check if config file exists
$configFile = __DIR__ . '/config.php';
$configExists = file_exists($configFile);

// If config doesn't exist and not installing, show installation wizard
if (!$configExists && (!isset($_GET['action']) || $_GET['action'] !== 'install')) {
    // Show installation wizard
    include_once 'install_wizard.php';
    exit;
}

// Load configuration if it exists
if ($configExists) {
    require_once $configFile;
    // Set globals for CLI workers
    $DB_HOST = defined('DB_HOST') ? DB_HOST : 'localhost';
    $DB_NAME = defined('DB_NAME') ? DB_NAME : '';
    $DB_USER = defined('DB_USER') ? DB_USER : '';
    $DB_PASS = defined('DB_PASS') ? DB_PASS : '';
} else {
    // Defaults for installation
    $DB_HOST = 'localhost';
    $DB_NAME = '';
    $DB_USER = '';
    $DB_PASS = '';
}

///////////////////////
//  CONSTANTS
///////////////////////
// Brand configuration
define('BRAND_NAME', 'MAFIA MAILER');

// Database connection timeout settings (in seconds)
// DB_OPERATION_TIMEOUT: Client-side query timeout (PDO::ATTR_TIMEOUT) - max time for queries to execute
// DB_SESSION_TIMEOUT: Server-side idle timeout (MySQL wait_timeout) - max idle time before MySQL closes connection
define('DB_OPERATION_TIMEOUT', 20); // Query execution timeout on client side
define('DB_SESSION_TIMEOUT', 20); // Session idle timeout on server side

// Memory configuration (for 64GB RAM server - utilize available RAM)
define('MEMORY_LIMIT_LARGE_SERVER', '2048M'); // 2GB per process is sufficient for loading large recipient lists
define('MEMORY_LIMIT_MINIMUM', '1024M'); // Minimum 1GB per worker for basic operations

// Worker configuration defaults (optimized for 64GB RAM SERVERS WITH RAM DISK)
// REVOLUTIONARY APPROACH: Use RAM disk (/dev/shm) to cache recipients, eliminating database bottleneck
// Strategy: Load recipients once to RAM, workers read from RAM files - NO database queries during send!
// For 1M emails: 500 total workers needed, spawned in 5 batches of 100
// With 64GB RAM: Can cache 100M+ email addresses in tmpfs (/dev/shm)
// Total RAM needed: 100 concurrent workers × 50MB = 5GB (recipient cache ~100MB for 1M emails)
// RECOMMENDATION: Ensure /dev/shm has sufficient space (typically 50% of RAM = 32GB on 64GB server)
// With RAM DISK MODE (MAX_CONCURRENT_WORKERS=100, 50ms spawn delay, RAM caching):
// Formula: NO database connection limits - workers read from RAM!
// - For 1M emails: Can use 100-500 workers safely
// - Recommended: max_connections = 50 (only for initial load, then workers use RAM)
// Also RECOMMENDED: increase open_files_limit=2000+ and table_open_cache=2000+
define('DEFAULT_WORKERS', 2); // 2 workers per 1,000 emails (each handles 500 emails)
define('DEFAULT_MESSAGES_PER_WORKER', 500); // 500 emails per worker for maximum parallelism
define('MIN_WORKERS', 1);
// Maximum workers set to 500 to fully utilize 64GB RAM server resources
// Each worker process uses ~50–100MB actual RSS memory for 500-email batches
// With 64GB RAM, 500 workers × 100MB = ~50GB — safely within the server's capacity
define('MAX_WORKERS', 500); // High limit to exploit 64GB RAM for maximum throughput
define('MIN_MESSAGES_PER_WORKER', 1);
define('MAX_MESSAGES_PER_WORKER', 10000);
// Warning threshold for large worker counts (for logging purposes)
define('WORKERS_WARNING_THRESHOLD', 1000); // Warn at higher threshold for large-scale operations

// Worker auto-calculation configuration: 2 workers per 1,000 emails, each handling 500 emails
// This achieves maximum parallelism: for 1,000 emails → 2 workers × 500 emails each
// For larger campaigns: 10,000 emails → 20 workers, 100,000 emails → 200 workers (all parallel)
define('WORKERS_PER_THOUSAND', 2); // 2 workers per 1,000 emails (500 emails per worker)
define('EMAILS_PER_WORKER_GROUP', 1000); // Email count grouping for worker calculation

// Connection Numbers configuration (MaxBulk Mailer style) - optimized for high throughput
define('DEFAULT_CONNECTIONS', 20); // Increased from 5 to 20 concurrent SMTP connections
define('MIN_CONNECTIONS', 1);
define('MAX_CONNECTIONS', 100); // Increased from 40 to 100 to fully utilize server resources

// Batch size configuration (emails per single SMTP connection) - optimized for speed
define('DEFAULT_BATCH_SIZE', 200); // Increased from 50 to 200 for faster batch processing
define('MIN_BATCH_SIZE', 1);
define('MAX_BATCH_SIZE', 1000); // Increased from 500 to 1000 for maximum throughput

// Number of emails after which each SMTP worker reports progress to the DB.
// With 117 workers each reporting every 50 emails, the UI sees 117 × 50 = 5,850 new
// emails per "tick" — well above the requested 10 batches × 500 = 5,000/second target.
// Setting this too low adds DB write overhead; 50 is a safe sweet-spot.
define('SMTP_PROGRESS_CHUNK_SIZE', 50);

// Progress update frequency (update progress every N emails)
// CRITICAL FIX: Reduced from 50 to 10 to ensure more frequent progress updates
// This helps prevent "stuck" campaign appearance by updating database more often
define('PROGRESS_UPDATE_FREQUENCY', 10);

// Worker heartbeat configuration
// CRITICAL FIX: Reduced frequency from 20 to 5 to prevent "0 workers" display issue
// Workers now log heartbeats more frequently so dashboard can properly detect active workers
define('WORKER_HEARTBEAT_FREQUENCY', 5); // Log heartbeat every 5 emails processed (was 20)
define('WORKER_HEARTBEAT_WINDOW', 60); // Consider workers active if heartbeat within 60 seconds (was 30)

// Worker spawning configuration (all workers launch simultaneously for maximum speed)
// With 64GB RAM server, no need to batch-spawn workers - launch all at once
// All workers start in parallel immediately with zero delay
define('MAX_CONCURRENT_WORKERS', 500); // All workers run simultaneously (no batching)
define('WORKER_SPAWN_BATCH_DELAY_MS', 0); // No delay - all workers launch at once for full parallelism

// Cycle delay configuration (delay between worker processing cycles in milliseconds)
// Set to 0 for maximum speed - no delays between processing cycles
define('DEFAULT_CYCLE_DELAY_MS', 0); // 0 = maximum speed, no delays
define('MIN_CYCLE_DELAY_MS', 0);
define('MAX_CYCLE_DELAY_MS', 10000); // Max 10 seconds

// Operation lock timeouts (for fetch/send separation)
// SEND_LOCK_TIMEOUT: Timeout for acquiring send operation lock
// FETCH_LOCK_TIMEOUT: Timeout for acquiring fetch operation lock
// Shorter timeouts for non-blocking behavior - operations should not wait long
define('SEND_LOCK_TIMEOUT', 10); // 10 seconds timeout for send operations
define('FETCH_LOCK_TIMEOUT', 5); // 5 seconds timeout for fetch operations (faster fail for background tasks)

// Worker cleanup configuration
define('WORKER_WAIT_TIMEOUT_SECONDS', 300); // 5 minutes maximum wait time for workers
define('WORKER_POLL_INTERVAL_US', 100000); // 100ms polling interval (in microseconds)

// Chart configuration
define('DEFAULT_CHART_DAYS', 30); // Default number of days to show in charts

// RAM disk configuration for high-performance recipient caching
// For servers with 64GB+ RAM, use RAM disk to store recipients and eliminate DB bottleneck
define('USE_RAM_DISK', true); // Enable RAM-based recipient storage
define('RAM_DISK_PATH', '/dev/shm'); // Linux tmpfs - RAM-based filesystem (typically 50% of RAM)
define('RAM_DISK_FALLBACK', true); // If RAM disk unavailable, fallback to regular temp dir
define('MAX_CHART_DAYS', 365); // Maximum number of days allowed in charts

// Download configuration
define('MAX_DOWNLOAD_RECORDS', 50000); // Maximum number of records per CSV download

// API batch delivery configuration (curl_multi pool with true multi-recipient requests)
// API_RECIPIENTS_PER_REQUEST: recipients bundled into one API call (SendGrid personalizations[].to[])
// API_MULTI_POOL_SIZE: number of persistent cURL handles kept alive in the curl_multi pool
// API_BATCH_INTER_REQUEST_DELAY_US: microseconds to wait before re-feeding each handle (0.5 ms = 500 µs)
//   prevents API rate-limiting when the pool fires requests in rapid succession
define('API_RECIPIENTS_PER_REQUEST', 500);           // 500 is safe for all major providers (SendGrid max=1000)
define('API_MULTI_POOL_SIZE', 8);                    // Concurrent handles per worker; reused across batches
define('API_BATCH_INTER_REQUEST_DELAY_US', 500);     // 500 µs = 0.5 ms between batch submissions (prevents rate-limiting)

///////////////////////
//  MEMORY LIMIT SETUP
///////////////////////
// Increase memory limit for large server operations
$currentMemoryLimit = ini_get('memory_limit');
$currentMemoryBytes = return_bytes($currentMemoryLimit);
$targetMemoryBytes = return_bytes(MEMORY_LIMIT_LARGE_SERVER);

// Set to 8GB if current limit is lower (for large servers)
if ($currentMemoryLimit !== '-1' && $currentMemoryBytes < $targetMemoryBytes) {
    @ini_set('memory_limit', MEMORY_LIMIT_LARGE_SERVER);
    error_log("MAFIA MAILER: Increased memory limit from {$currentMemoryLimit} to " . MEMORY_LIMIT_LARGE_SERVER . " for large server operations");
}

///////////////////////
//  DATABASE CONNECTION
///////////////////////
$pdo = null;
if ($DB_NAME !== '' && $DB_USER !== '') {
    try {
        $pdo = new PDO(
            "mysql:host={$DB_HOST};dbname={$DB_NAME};charset=utf8mb4",
            $DB_USER,
            $DB_PASS,
            [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_TIMEOUT => DB_OPERATION_TIMEOUT,
                PDO::ATTR_PERSISTENT => false, // Non-persistent to ensure clean connections
                PDO::MYSQL_ATTR_INIT_COMMAND => "SET SESSION wait_timeout=" . DB_SESSION_TIMEOUT . ", SESSION interactive_timeout=" . DB_SESSION_TIMEOUT
            ]
        );
    } catch (Exception $e) {
        $errorMsg = $e->getMessage();
        $isTooManyConnections = (strpos($errorMsg, '1040') !== false || strpos($errorMsg, 'Too many connections') !== false);
        
        if (PHP_SAPI === 'cli') {
            if ($isTooManyConnections) {
                fwrite(STDERR, "CRITICAL: MySQL connection pool exhausted (too many connections)\n");
                fwrite(STDERR, "This means the database has reached its max_connections limit.\n");
                fwrite(STDERR, "SOLUTION 1: Increase max_connections in /etc/mysql/my.cnf (recommended: 200+)\n");
                fwrite(STDERR, "SOLUTION 2: Wait for other campaigns/workers to complete\n");
                fwrite(STDERR, "SOLUTION 3: Reduce MAX_CONCURRENT_WORKERS in mail.php (currently: " . MAX_CONCURRENT_WORKERS . ")\n");
            }
            fwrite(STDERR, "DB connection error: " . $errorMsg . PHP_EOL);
            exit(1);
        }
        // If web request and not installing, show detailed error
        if (!isset($_GET['action']) || $_GET['action'] !== 'install') {
            $errorDetails = htmlspecialchars($errorMsg);
            $configPath = htmlspecialchars(__DIR__ . '/config.php');
            
            // Enhanced error message for "too many connections"
            $additionalHelp = '';
            if ($isTooManyConnections) {
                $additionalHelp = '
                <div style="background: #fff3cd; border: 2px solid #ffc107; border-radius: 8px; padding: 20px; margin: 20px 0;">
                    <h3 style="color: #856404; margin-top: 0;">⚠️ Database Connection Pool Exhausted</h3>
                    <p style="color: #856404; margin: 10px 0;">The database has reached its maximum number of connections (max_connections limit).</p>
                    <p style="color: #856404; margin: 10px 0;"><strong>This happens when:</strong></p>
                    <ul style="color: #856404;">
                        <li>Multiple campaigns are running simultaneously</li>
                        <li>MySQL max_connections is set too low</li>
                        <li>Too many workers are spawning at once</li>
                    </ul>
                    <p style="color: #856404; margin: 10px 0;"><strong>Solutions:</strong></p>
                    <ol style="color: #856404;">
                        <li><strong>Wait:</strong> Let current campaigns complete, then try again</li>
                        <li><strong>Increase MySQL limit:</strong> Edit <code>/etc/mysql/my.cnf</code> and set <code>max_connections=200</code> (or higher), then restart MySQL</li>
                        <li><strong>Reduce workers:</strong> Edit mail.php and reduce MAX_CONCURRENT_WORKERS (currently: ' . MAX_CONCURRENT_WORKERS . ')</li>
                    </ol>
                </div>';
            }
            
            die('
            <!DOCTYPE html>
            <html>
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Database Connection Error - ' . BRAND_NAME . '</title>
                <style>
                    body { 
                        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
                        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                        display: flex;
                        align-items: center;
                        justify-content: center;
                        min-height: 100vh;
                        margin: 0;
                        padding: 20px;
                    }
                    .error-box {
                        background: white;
                        border-radius: 10px;
                        padding: 40px;
                        max-width: 600px;
                        box-shadow: 0 10px 40px rgba(0,0,0,0.2);
                    }
                    h1 {
                        color: #e74c3c;
                        margin-top: 0;
                        font-size: 24px;
                    }
                    .error-icon {
                        font-size: 48px;
                        text-align: center;
                        margin-bottom: 20px;
                    }
                    .error-details {
                        background: #f8f9fa;
                        border-left: 4px solid #e74c3c;
                        padding: 15px;
                        margin: 20px 0;
                        font-family: monospace;
                        font-size: 14px;
                        word-wrap: break-word;
                    }
                    .help-section {
                        background: #e8f4f8;
                        border-left: 4px solid #3498db;
                        padding: 15px;
                        margin: 20px 0;
                    }
                    .help-section h3 {
                        margin-top: 0;
                        color: #2c3e50;
                        font-size: 16px;
                    }
                    .help-section ul {
                        margin: 10px 0;
                        padding-left: 20px;
                    }
                    .help-section li {
                        margin: 5px 0;
                    }
                    code {
                        background: #f4f4f4;
                        padding: 2px 6px;
                        border-radius: 3px;
                        font-family: monospace;
                    }
                    .btn {
                        display: inline-block;
                        background: #3498db;
                        color: white;
                        padding: 10px 20px;
                        text-decoration: none;
                        border-radius: 5px;
                        margin-top: 20px;
                    }
                    .btn:hover {
                        background: #2980b9;
                    }
                </style>
            </head>
            <body>
                <div class="error-box">
                    <div class="error-icon">⚠️</div>
                    <h1>Database Connection Error</h1>
                    <p>Unable to connect to the database. Please check your configuration.</p>
                    
                    <div class="error-details">
                        <strong>Error:</strong> ' . $errorDetails . '
                    </div>
                    
                    <div class="help-section">
                        <h3>🔧 How to Fix:</h3>
                        <ul>
                            <li>Check your database credentials in <code>' . $configPath . '</code></li>
                            <li>Verify the database server is running</li>
                            <li>Ensure the database name exists</li>
                            <li>Confirm the user has proper permissions</li>
                            <li>Check if MySQL/MariaDB service is active</li>
                        </ul>
                    </div>
                    
                    <div class="help-section">
                        <h3>💾 Server Configuration:</h3>
                        <ul>
                            <li><strong>Host:</strong> <code>' . htmlspecialchars($DB_HOST) . '</code></li>
                            <li><strong>Database:</strong> <code>' . htmlspecialchars($DB_NAME) . '</code></li>
                            <li><strong>User:</strong> <code>' . htmlspecialchars($DB_USER) . '</code></li>
                            <li><strong>Memory Limit:</strong> <code>' . ini_get('memory_limit') . '</code></li>
                        </ul>
                    </div>
                    
                    <a href="?action=install" class="btn">Run Installation Wizard</a>
                </div>
            </body>
            </html>
            ');
        }
    }
}

if (PHP_SAPI === 'cli' && isset($argv) && count($argv) > 1) {
    if ($argv[1] === '--bg-send-profile') {
        $cid = isset($argv[2]) ? (int)$argv[2] : 0;
        $pid = isset($argv[3]) ? (int)$argv[3] : 0;
        $tmpfile = isset($argv[4]) ? $argv[4] : '';
        $recipientsText = '';
        $overrides = [];
        if ($tmpfile && is_readable($tmpfile)) {
            $raw = file_get_contents($tmpfile);
            $dec = json_decode($raw, true);
            if (is_array($dec) && isset($dec['recipients'])) {
                // support recipients as array or string
                if (is_array($dec['recipients'])) {
                    $recipientsText = implode("\n", $dec['recipients']);
                } else {
                    $recipientsText = (string)$dec['recipients'];
                }
                $overrides = isset($dec['overrides']) && is_array($dec['overrides']) ? $dec['overrides'] : [];
            } else {
                $recipientsText = $raw;
            }
        }
        
        // CRITICAL FIX: Close parent's connection and create fresh one for this CLI worker
        // This prevents "too many connections" at start when many workers spawn
        $pdo = null;
        
        // Create new connection with retry logic
        $maxRetries = 5;
        $pdo = null;
        for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
            try {
                $pdo = new PDO(
                    "mysql:host={$DB_HOST};dbname={$DB_NAME};charset=utf8mb4",
                    $DB_USER,
                    $DB_PASS,
                    [
                        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                        PDO::ATTR_TIMEOUT => DB_OPERATION_TIMEOUT,
                        PDO::ATTR_PERSISTENT => false,
                        PDO::MYSQL_ATTR_INIT_COMMAND => "SET SESSION wait_timeout=" . DB_SESSION_TIMEOUT . ", SESSION interactive_timeout=" . DB_SESSION_TIMEOUT
                    ]
                );
                error_log("CLI worker (bg-send-profile) PID " . getmypid() . ": Created fresh DB connection (attempt $attempt)");
                break;
            } catch (Exception $e) {
                $errorMsg = $e->getMessage();
                $isTooManyConnections = (strpos($errorMsg, '1040') !== false || strpos($errorMsg, 'Too many connections') !== false);
                
                if ($attempt >= $maxRetries) {
                    error_log("CLI worker (bg-send-profile) PID " . getmypid() . ": FAILED to connect after $maxRetries attempts: $errorMsg");
                    if ($tmpfile) @unlink($tmpfile);
                    exit(1);
                }
                
                // Exponential backoff with jitter for connection retries
                // Increased base delay from 100ms to 200ms to spread connection attempts more
                $backoffMs = (200 * pow(2, $attempt - 1)) + random_int(0, 200);
                error_log("CLI worker (bg-send-profile) PID " . getmypid() . ": Connection attempt $attempt failed, retrying in {$backoffMs}ms...");
                usleep($backoffMs * 1000);
            }
        }
        
        try {
            $campaign = get_campaign($pdo, $cid);
            if ($campaign) {
                send_campaign_real($pdo, $campaign, $recipientsText, false, $pid, $overrides);
            }
        } catch (Exception $e) {
            error_log("CLI worker error (bg-send-profile) for campaign $cid (profile $pid): " . $e->getMessage());
        } finally {
            // Explicitly close database connection to prevent connection leaks
            $pdo = null;
        }
        if ($tmpfile) @unlink($tmpfile);
        exit(0);
    }

    if ($argv[1] === '--bg-send') {
        $cid = isset($argv[2]) ? (int)$argv[2] : 0;
        $tmpfile = isset($argv[3]) ? $argv[3] : '';
        $recipientsText = '';
        $overrides = [];
        if ($tmpfile && is_readable($tmpfile)) {
            $raw = file_get_contents($tmpfile);
            $dec = json_decode($raw, true);
            if (is_array($dec) && isset($dec['recipients'])) {
                if (is_array($dec['recipients'])) {
                    $recipientsText = implode("\n", $dec['recipients']);
                } else {
                    $recipientsText = (string)$dec['recipients'];
                }
                $overrides = isset($dec['overrides']) && is_array($dec['overrides']) ? $dec['overrides'] : [];
            } else {
                $recipientsText = $raw;
            }
        }
        
        // CRITICAL FIX: Close parent's connection and create fresh one for this CLI worker
        // This prevents "too many connections" at start when many workers spawn
        $pdo = null;
        
        // Create new connection with retry logic
        $maxRetries = 5;
        $pdo = null;
        for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
            try {
                $pdo = new PDO(
                    "mysql:host={$DB_HOST};dbname={$DB_NAME};charset=utf8mb4",
                    $DB_USER,
                    $DB_PASS,
                    [
                        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                        PDO::ATTR_TIMEOUT => DB_OPERATION_TIMEOUT,
                        PDO::ATTR_PERSISTENT => false,
                        PDO::MYSQL_ATTR_INIT_COMMAND => "SET SESSION wait_timeout=" . DB_SESSION_TIMEOUT . ", SESSION interactive_timeout=" . DB_SESSION_TIMEOUT
                    ]
                );
                error_log("CLI worker (bg-send) PID " . getmypid() . ": Created fresh DB connection (attempt $attempt)");
                break;
            } catch (Exception $e) {
                $errorMsg = $e->getMessage();
                $isTooManyConnections = (strpos($errorMsg, '1040') !== false || strpos($errorMsg, 'Too many connections') !== false);
                
                if ($attempt >= $maxRetries) {
                    error_log("CLI worker (bg-send) PID " . getmypid() . ": FAILED to connect after $maxRetries attempts: $errorMsg");
                    if ($tmpfile) @unlink($tmpfile);
                    exit(1);
                }
                
                // Exponential backoff with jitter for connection retries
                // Increased base delay from 100ms to 200ms to spread connection attempts more
                $backoffMs = (200 * pow(2, $attempt - 1)) + random_int(0, 200);
                error_log("CLI worker (bg-send) PID " . getmypid() . ": Connection attempt $attempt failed, retrying in {$backoffMs}ms...");
                usleep($backoffMs * 1000);
            }
        }
        
        try {
            $campaign = get_campaign($pdo, $cid);
            if ($campaign) {
                send_campaign_real($pdo, $campaign, $recipientsText, false, null, $overrides);
            }
        } catch (Exception $e) {
            error_log("CLI worker error (bg-send) for campaign $cid: " . $e->getMessage());
        } finally {
            // Explicitly close database connection to prevent connection leaks
            $pdo = null;
        }
        if ($tmpfile) @unlink($tmpfile);
        exit(0);
    }

    if ($argv[1] === '--bg-scan-bounces') {
        // CRITICAL FIX: Close parent's connection and create fresh one for this CLI worker
        $pdo = null;
        
        // Create new connection with retry logic
        $maxRetries = 3;
        for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
            try {
                $pdo = new PDO(
                    "mysql:host={$DB_HOST};dbname={$DB_NAME};charset=utf8mb4",
                    $DB_USER,
                    $DB_PASS,
                    [
                        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                        PDO::ATTR_TIMEOUT => DB_OPERATION_TIMEOUT,
                        PDO::ATTR_PERSISTENT => false,
                        PDO::MYSQL_ATTR_INIT_COMMAND => "SET SESSION wait_timeout=" . DB_SESSION_TIMEOUT . ", SESSION interactive_timeout=" . DB_SESSION_TIMEOUT
                    ]
                );
                error_log("CLI worker (bg-scan-bounces) PID " . getmypid() . ": Created fresh DB connection");
                break;
            } catch (Exception $e) {
                if ($attempt >= $maxRetries) {
                    error_log("CLI worker (bg-scan-bounces) PID " . getmypid() . ": FAILED to connect after $maxRetries attempts: " . $e->getMessage());
                    exit(1);
                }
                // Increased base delay from 100ms to 200ms for better spacing
                $backoffMs = (200 * pow(2, $attempt - 1)) + random_int(0, 200);
                error_log("CLI worker (bg-scan-bounces) PID " . getmypid() . ": Connection attempt $attempt failed, retrying in {$backoffMs}ms...");
                usleep($backoffMs * 1000);
            }
        }
        
        try {
            process_imap_bounces($pdo);
        } catch (Exception $e) {
            error_log("Bounce scan error: " . $e->getMessage());
        } finally {
            // Explicitly close database connection to prevent connection leaks
            $pdo = null;
        }
        exit(0);
    }
}

// Check database connection limits for large-scale operations (if database is connected)
if ($pdo !== null && PHP_SAPI === 'cli') {
    check_database_connection_limits($pdo);
}

session_start();

///////////////////////
//  AUTHENTICATION SYSTEM
///////////////////////
// Check if user is logged in (skip for login, logout, install, and tracking actions)
$publicActions = ['login', 'do_login', 'logout', 'install', 'do_install'];
$trackingRequests = ['t' => ['open', 'click', 'unsubscribe']];
$isPublicAction = isset($_GET['action']) && in_array($_GET['action'], $publicActions);
$isTrackingRequest = isset($_GET['t']) && in_array($_GET['t'], $trackingRequests['t']);
$isApiRequest = isset($_GET['api']);

if (!$isPublicAction && !$isTrackingRequest && !$isApiRequest && PHP_SAPI !== 'cli') {
    // Check if installation is complete
    if (!defined('INSTALLED') || !INSTALLED) {
        // Redirect to installation wizard
        if (!isset($_GET['action']) || $_GET['action'] !== 'install') {
            header('Location: ' . $_SERVER['SCRIPT_NAME'] . '?action=install');
            exit;
        }
    } elseif (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_logged_in'] !== true) {
        // Not logged in
        if (!isset($_GET['action']) || $_GET['action'] !== 'login') {
            // Check if 'lo' parameter is present to allow login redirect
            // Matches: ?lo, ?lo=, ?lo&..., ?=lo, ?=lo&..., &lo, &lo=, &=lo
            // Uses regex to ensure 'lo' is matched as a complete parameter name, not substring
            $queryString = $_SERVER['QUERY_STRING'] ?? '';
            $hasLoParam = isset($_GET['lo']) || preg_match('/(?:^|&)=?lo(?:=|&|$)/', $queryString);
            
            if ($hasLoParam) {
                // Redirect to login page
                header('Location: ' . $_SERVER['SCRIPT_NAME'] . '?action=login');
                exit;
            } else {
                // Show access denied message
                http_response_code(403);
                echo '<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Access Denied</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        }
        .access-denied {
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.2);
            text-align: center;
            max-width: 400px;
        }
        .access-denied h1 {
            color: #d32f2f;
            margin: 0 0 20px 0;
            font-size: 48px;
        }
        .access-denied h2 {
            color: #333;
            margin: 0 0 15px 0;
            font-size: 24px;
        }
        .access-denied p {
            color: #666;
            line-height: 1.6;
        }
    </style>
</head>
<body>
    <div class="access-denied">
        <h1>403</h1>
        <h2>Access Denied</h2>
        <p>You do not have permission to access this resource.</p>
    </div>
</body>
</html>';
                exit;
            }
        }
    }
}

///////////////////////
//  AUTHENTICATION HANDLERS
///////////////////////
if (isset($_GET['action']) && $_GET['action'] === 'do_login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = $_POST['username'] ?? '';
    $password = $_POST['password'] ?? '';
    
    if (defined('ADMIN_USERNAME') && defined('ADMIN_PASSWORD_HASH')) {
        if ($username === ADMIN_USERNAME && password_verify($password, ADMIN_PASSWORD_HASH)) {
            $_SESSION['admin_logged_in'] = true;
            $_SESSION['admin_username'] = $username;
            header('Location: ' . $_SERVER['SCRIPT_NAME'] . '?page=dashboard');
            exit;
        }
    }
    
    // Login failed
    header('Location: ' . $_SERVER['SCRIPT_NAME'] . '?action=login&error=1');
    exit;
}

if (isset($_GET['action']) && $_GET['action'] === 'logout') {
    session_destroy();
    header('Location: ' . $_SERVER['SCRIPT_NAME'] . '?action=login&logged_out=1');
    exit;
}

// Show login page
if (isset($_GET['action']) && $_GET['action'] === 'login') {
    include_once 'login.php';
    exit;
}

// Show installation wizard
if (isset($_GET['action']) && $_GET['action'] === 'install') {
    include_once 'install_wizard.php';
    exit;
}

///////////////////////
//  OPTIONAL SCHEMA (Contacts + added columns)
///////////////////////
// Only run schema updates if PDO is connected and user is authenticated
if ($pdo !== null && (isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in'] === true)) {
    try {
        $pdo->exec("
            CREATE TABLE IF NOT EXISTS contact_lists (
                id INT AUTO_INCREMENT PRIMARY KEY,
                name VARCHAR(255) NOT NULL,
                type ENUM('global','list') NOT NULL DEFAULT 'list',
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
        ");
        $pdo->exec("
            CREATE TABLE IF NOT EXISTS contacts (
                id INT AUTO_INCREMENT PRIMARY KEY,
                list_id INT NOT NULL,
                email VARCHAR(255) NOT NULL,
                first_name VARCHAR(100) DEFAULT NULL,
                last_name VARCHAR(100) DEFAULT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                INDEX(list_id),
                INDEX(email)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
        ");

        $pdo->exec("
            CREATE TABLE IF NOT EXISTS unsubscribes (
                email VARCHAR(255) PRIMARY KEY,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
        ");

        // Create global settings table for tracking configuration
        $pdo->exec("
            CREATE TABLE IF NOT EXISTS settings (
                setting_key VARCHAR(100) PRIMARY KEY,
                setting_value TEXT,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
        ");
        
        // Initialize default tracking settings
        try {
            $pdo->exec("INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('open_tracking_enabled', '1')");
            $pdo->exec("INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('click_tracking_enabled', '1')");
        } catch (Exception $e) {}

        // Ensure sending_profiles has useful columns
        try { $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS sender_name VARCHAR(255) NOT NULL DEFAULT ''"); } catch (Exception $e) {}
        // Add reply_to column for custom Reply-To address
        try { $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS reply_to VARCHAR(255) NOT NULL DEFAULT ''"); } catch (Exception $e) {}
        // Add provider column for profile identification
        try { $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS provider VARCHAR(100) NOT NULL DEFAULT 'Custom'"); } catch (Exception $e) {}
        // Add connection_numbers field (MaxBulk Mailer style - concurrent SMTP connections)
        // Optimized default for high-performance 128GB RAM servers
        try { $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS connection_numbers INT NOT NULL DEFAULT 20"); } catch (Exception $e) {}
        // Add batch_size field (number of emails to send per single SMTP connection)
        // Optimized default for maximum throughput
        try { $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS batch_size INT NOT NULL DEFAULT 200"); } catch (Exception $e) {}

        try { $pdo->exec("ALTER TABLE campaigns ADD COLUMN IF NOT EXISTS unsubscribe_enabled TINYINT(1) NOT NULL DEFAULT 0"); } catch (Exception $e) {}
        try { $pdo->exec("ALTER TABLE campaigns ADD COLUMN IF NOT EXISTS ref_injection_enabled TINYINT(1) NOT NULL DEFAULT 1"); } catch (Exception $e) {}
        try { $pdo->exec("ALTER TABLE campaigns ADD COLUMN IF NOT EXISTS preheader_injection_enabled TINYINT(1) NOT NULL DEFAULT 1"); } catch (Exception $e) {}
        try { $pdo->exec("ALTER TABLE campaigns ADD COLUMN IF NOT EXISTS footer_injection_enabled TINYINT(1) NOT NULL DEFAULT 1"); } catch (Exception $e) {}
        try { $pdo->exec("ALTER TABLE campaigns ADD COLUMN IF NOT EXISTS sender_name VARCHAR(255) NOT NULL DEFAULT ''"); } catch (Exception $e) {}
        // Add reply_to column for campaign-level Reply-To override
        try { $pdo->exec("ALTER TABLE campaigns ADD COLUMN IF NOT EXISTS reply_to VARCHAR(255) NOT NULL DEFAULT ''"); } catch (Exception $e) {}
        // Add tracking settings columns
        try { $pdo->exec("ALTER TABLE campaigns ADD COLUMN IF NOT EXISTS open_tracking_enabled TINYINT(1) NOT NULL DEFAULT 1"); } catch (Exception $e) {}
        try { $pdo->exec("ALTER TABLE campaigns ADD COLUMN IF NOT EXISTS click_tracking_enabled TINYINT(1) NOT NULL DEFAULT 1"); } catch (Exception $e) {}
        // Add campaign progress tracking fields
        try { 
            $pdo->exec("ALTER TABLE campaigns ADD COLUMN IF NOT EXISTS progress_sent INT NOT NULL DEFAULT 0");
            $pdo->exec("ALTER TABLE campaigns ADD COLUMN IF NOT EXISTS progress_total INT NOT NULL DEFAULT 0");
            $pdo->exec("ALTER TABLE campaigns ADD COLUMN IF NOT EXISTS progress_status VARCHAR(50) DEFAULT 'draft'");
        } catch (Exception $e) {}
        try {
            $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS bounce_imap_server VARCHAR(255) DEFAULT ''");
            $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS bounce_imap_user VARCHAR(255) DEFAULT ''");
            $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS bounce_imap_pass VARCHAR(255) DEFAULT ''");
        } catch (Exception $e) {}
        
        // Keep old fields for backward compatibility but they won't be shown in UI
        try { $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS send_rate INT NOT NULL DEFAULT 0"); } catch (Exception $e) {}
        try { $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS sends_used INT NOT NULL DEFAULT 0"); } catch (Exception $e) {}
        try {
            $pdo->exec("ALTER TABLE rotation_settings ADD COLUMN IF NOT EXISTS workers INT NOT NULL DEFAULT " . DEFAULT_WORKERS);
            $pdo->exec("ALTER TABLE rotation_settings ADD COLUMN IF NOT EXISTS messages_per_worker INT NOT NULL DEFAULT " . DEFAULT_MESSAGES_PER_WORKER);
        } catch (Exception $e) {}
        try {
            $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS workers INT NOT NULL DEFAULT " . DEFAULT_WORKERS);
            $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS messages_per_worker INT NOT NULL DEFAULT " . DEFAULT_MESSAGES_PER_WORKER);
            $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS cycle_delay_ms INT NOT NULL DEFAULT " . DEFAULT_CYCLE_DELAY_MS);
        } catch (Exception $e) {}
        // Proxy support columns
        try { $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS proxy_enabled TINYINT(1) NOT NULL DEFAULT 0"); } catch (Exception $e) {}
        try { $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS proxy_type VARCHAR(10) NOT NULL DEFAULT 'http'"); } catch (Exception $e) {}
        try { $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS proxy_host VARCHAR(255) NOT NULL DEFAULT ''"); } catch (Exception $e) {}
        try { $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS proxy_port INT NOT NULL DEFAULT 8080"); } catch (Exception $e) {}
        try { $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS proxy_user VARCHAR(255) NOT NULL DEFAULT ''"); } catch (Exception $e) {}
        try { $pdo->exec("ALTER TABLE sending_profiles ADD COLUMN IF NOT EXISTS proxy_pass VARCHAR(255) NOT NULL DEFAULT ''"); } catch (Exception $e) {}
        
        // PERFORMANCE OPTIMIZATION: Add database indexes for frequently queried columns
        // These indexes significantly improve query performance for campaign stats and progress tracking
        try {
            // Index on campaigns table for status and progress queries
            $pdo->exec("ALTER TABLE campaigns ADD INDEX IF NOT EXISTS idx_status (status)");
            $pdo->exec("ALTER TABLE campaigns ADD INDEX IF NOT EXISTS idx_progress_status (progress_status)");
            $pdo->exec("ALTER TABLE campaigns ADD INDEX IF NOT EXISTS idx_status_progress (status, progress_status)");
            
            // Index on events table for campaign stats queries (most critical for performance)
            $pdo->exec("ALTER TABLE events ADD INDEX IF NOT EXISTS idx_campaign_event (campaign_id, event_type)");
            $pdo->exec("ALTER TABLE events ADD INDEX IF NOT EXISTS idx_campaign_created (campaign_id, created_at)");
            
            // NOTE: Functional index on JSON_EXTRACT may have compatibility issues with older MySQL versions
            // Consider adding a dedicated worker_pid column in future schema updates for better performance
            // For now, this provides reasonable performance on MySQL 5.7+ with JSON support
            $pdo->exec("ALTER TABLE events ADD INDEX IF NOT EXISTS idx_worker_pid ((CAST(JSON_EXTRACT(details, '$.worker_pid') AS UNSIGNED)))");
            
            // Index for email searches in event details
            $pdo->exec("ALTER TABLE events ADD INDEX IF NOT EXISTS idx_event_type (event_type)");
        } catch (Exception $e) {
            error_log("Database index creation error: " . $e->getMessage());
        }
        
        // RETROACTIVE FIX: Convert stuck campaigns to 'sent'
        // Only use the time-based check (10 minutes of no worker activity) to avoid
        // prematurely marking a campaign as 'sent' while events are still being inserted.
        // The progress_sent >= progress_total check is intentionally omitted: for API sends,
        // progress_sent is updated via callbacks BEFORE event rows are inserted, so that
        // condition fires while the event insertion loop is still running.
        try {
            // Fix campaigns where progress_status is 'completed' but main status is not 'sent'.
            // progress_status = 'completed' is only set by the actual sending code after all
            // event rows have been written, so this check is safe.
            $pdo->exec("
                UPDATE campaigns 
                SET status = 'sent', 
                    sent_at = COALESCE(sent_at, NOW())
                WHERE progress_status = 'completed'
                  AND status != 'sent'
            ");
            
            // CRITICAL FIX: Mark campaigns as completed if no workers have been active for 10 minutes
            // Increased from 2 minutes to 10 minutes to prevent premature completion
            // This allows slow-processing workers to complete without campaign being marked as done
            // Now checks for worker_start and worker_complete events too
            $pdo->exec("
                UPDATE campaigns c
                LEFT JOIN (
                    SELECT campaign_id, MAX(created_at) as last_activity
                    FROM events
                    WHERE event_type IN ('worker_start', 'worker_heartbeat', 'worker_complete', 'delivered', 'bounce', 'deferred')
                    AND JSON_EXTRACT(details, '$.worker_pid') IS NOT NULL
                    GROUP BY campaign_id
                ) e ON c.id = e.campaign_id
                SET c.status = 'sent',
                    c.sent_at = COALESCE(c.sent_at, NOW()),
                    c.progress_status = 'completed'
                WHERE c.status IN ('sending', 'queued')
                  AND c.progress_sent > 0
                  AND (
                    e.last_activity IS NULL 
                    OR e.last_activity < DATE_SUB(NOW(), INTERVAL 10 MINUTE)
                  )
                  AND c.updated_at < DATE_SUB(NOW(), INTERVAL 10 MINUTE)
            ");
        } catch (Exception $e) {
            error_log("Campaign stuck fix error: " . $e->getMessage());
        }
    } catch (Exception $e) {
        // Log error but don't stop execution
        error_log("Schema update error: " . $e->getMessage());
    }
}

///////////////////////
//  HELPERS
///////////////////////

/**
 * Check database max_connections setting and log warning if insufficient for large campaigns
 * This is critical when running with high worker counts
 */
function check_database_connection_limits(PDO $pdo) {
    try {
        $stmt = $pdo->query("SHOW VARIABLES LIKE 'max_connections'");
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        if ($result) {
            $maxConnections = (int)$result['Value'];
            // Use 50% of MAX_WORKERS as typical usage threshold (more realistic than 100%)
            // Most campaigns won't hit MAX_WORKERS, so warn based on reasonable usage
            $typicalWorkers = (int)(MAX_WORKERS * 0.5);
            $recommendedMin = (int)($typicalWorkers * 1.2);
            
            if ($maxConnections < $recommendedMin) {
                error_log("WARNING: MySQL max_connections is $maxConnections, recommended minimum is $recommendedMin for typical large campaigns.");
                error_log("Calculation: Based on 50% of MAX_WORKERS ($typicalWorkers workers) × 1.2 overhead = $recommendedMin connections.");
                error_log("To fix: Edit /etc/mysql/my.cnf and set max_connections=$recommendedMin (or higher), then restart MySQL.");
                error_log("Note: For maximum capacity (" . MAX_WORKERS . " workers), set max_connections=" . (int)(MAX_WORKERS * 1.2));
            } else {
                error_log("Database connection limit check: max_connections=$maxConnections (sufficient for up to ~" . floor($maxConnections / 1.2) . " workers)");
            }
        }
    } catch (Exception $e) {
        // Silently continue if check fails - not critical
    }
}

function h($s) {
    return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
}

function uuidv4()
{
    $data = random_bytes(16);
    $data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
    $data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}

function base64url_encode(string $data): string {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function base64url_decode(string $data): string {
    $data = strtr($data, '-_', '+/');
    return base64_decode($data);
}

function get_base_url(): string {
    $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
    $host   = $_SERVER['HTTP_HOST'] ?? 'localhost';
    $script = $_SERVER['SCRIPT_NAME'] ?? '/mailer.php';
    return $scheme . '://' . $host . $script;
}

function encode_mime_header(string $str): string {
    if ($str === '') return '';
    
    // Check if string contains any non-ASCII characters or special characters that need encoding
    // According to RFC 2047, we need to encode if there are:
    // - Non-ASCII characters (outside \x20-\x7E range)
    // - Special characters like quotes, parentheses, backslashes, etc.
    // - Characters that have special meaning in email headers
    
    // Check for any character that requires encoding:
    // - Any byte > 127 (non-ASCII, includes UTF-8 multibyte chars, emojis, symbols)
    // - Special RFC 5322 characters: ( ) < > @ , ; : \ " [ ]
    // - Control characters (< \x20)
    if (preg_match('/[\x00-\x1F\x7F-\xFF()<>@,;:\\\"\[\]]/', $str)) {
        // Use RFC 2047 Base64 encoding for the entire string
        // Format: =?charset?encoding?encoded-text?=
        // We use UTF-8 charset and Base64 (B) encoding
        
        // RFC 2047 recommends that encoded-words should not be longer than 75 characters
        // If the string is very long, we may need to split it, but for sender names
        // this is typically not an issue as they're usually short
        
        $encoded = base64_encode($str);
        
        // Check if we need to split into multiple encoded-words
        // Each encoded-word has overhead: =?UTF-8?B? (11 chars) + ?= (2 chars) = 13 chars
        // So we have 75 - 13 = 62 characters available for the encoded content
        $maxEncodedLength = 62;
        
        if (strlen($encoded) <= $maxEncodedLength) {
            // Single encoded-word is sufficient
            return '=?UTF-8?B?' . $encoded . '?=';
        }
        
        // For very long strings, split into multiple encoded-words
        // Each chunk should be on a valid UTF-8 boundary
        // Use mb_substr for safe UTF-8 character boundary detection
        
        // Calculate chunk size in original bytes that fits in maxEncodedLength base64 chars
        // Base64 encoding increases size by 4/3, so original size = maxEncodedLength * 3/4
        $chunkSize = (int)floor($maxEncodedLength * 3 / 4);
        
        // Split the string using mb_substr to respect UTF-8 character boundaries
        $chunks = [];
        $strLenChars = mb_strlen($str, 'UTF-8');
        $offset = 0;
        
        while ($offset < $strLenChars) {
            // Calculate how many UTF-8 characters we can fit
            // Start with an estimate and adjust based on byte length
            $chunkChars = $chunkSize;
            $chunk = mb_substr($str, $offset, $chunkChars, 'UTF-8');
            
            // If chunk is too large in bytes, reduce character count
            while (strlen($chunk) > $chunkSize && $chunkChars > 1) {
                $chunkChars--;
                $chunk = mb_substr($str, $offset, $chunkChars, 'UTF-8');
            }
            
            // Ensure we make progress (at least 1 character)
            if (mb_strlen($chunk, 'UTF-8') === 0) {
                $chunk = mb_substr($str, $offset, 1, 'UTF-8');
            }
            
            $chunks[] = $chunk;
            $offset += mb_strlen($chunk, 'UTF-8');
        }
        
        // Encode each chunk and join with CRLF + space for RFC 5322 compliance
        // This ensures proper folding for long headers
        $encodedWords = [];
        foreach ($chunks as $chunk) {
            $encodedWords[] = '=?UTF-8?B?' . base64_encode($chunk) . '?=';
        }
        
        // Use space as separator (simpler and works for most email clients)
        // For extremely long headers, CRLF folding would be: implode("\r\n ", $encodedWords)
        return implode(' ', $encodedWords);
    }
    
    // String contains only safe ASCII characters, return as-is
    return $str;
}

/**
 * Convert PHP ini memory limit string to bytes
 */
function return_bytes(string $val): int {
    $val = trim($val);
    if ($val === '' || $val === '-1') {
        return PHP_INT_MAX; // No limit
    }
    
    $last = strtolower($val[strlen($val)-1]);
    $val = (int)$val;
    
    switch($last) {
        case 'g':
            $val *= 1024;
        case 'm':
            $val *= 1024;
        case 'k':
            $val *= 1024;
    }
    
    return $val;
}

function is_unsubscribed(PDO $pdo, string $email): bool {
    if ($email === '') return false;
    try {
        $stmt = $pdo->prepare("SELECT 1 FROM unsubscribes WHERE email = ? LIMIT 1");
        $stmt->execute([strtolower($email)]);
        return (bool)$stmt->fetchColumn();
    } catch (Exception $e) {
        return false;
    }
}

function add_to_unsubscribes(PDO $pdo, string $email) {
    if ($email === '') return;
    try {
        $stmt = $pdo->prepare("INSERT IGNORE INTO unsubscribes (email) VALUES (?)");
        $stmt->execute([strtolower($email)]);
    } catch (Exception $e) {
        // ignore
    }
}

/**
 * Check if geolocation API rate limit allows a new query
 * Rate limit: 10 IP addresses every 5 minutes to prevent hanging
 * Returns true if query is allowed, false otherwise
 */
function can_query_geolocation(): bool {
    $rateLimitFile = sys_get_temp_dir() . '/geolocation_rate_limit.json';
    $maxQueries = 10;
    $timeWindow = 300; // 5 minutes in seconds
    
    try {
        $now = time();
        $data = ['count' => 0, 'timestamp' => $now];
        
        // Read existing rate limit data
        if (file_exists($rateLimitFile)) {
            $content = @file_get_contents($rateLimitFile);
            if ($content !== false) {
                $decoded = json_decode($content, true);
                if (is_array($decoded) && isset($decoded['count']) && isset($decoded['timestamp'])) {
                    $data = $decoded;
                }
            }
        }
        
        // Check if time window has expired
        if (($now - $data['timestamp']) >= $timeWindow) {
            // Reset counter for new time window
            $data = ['count' => 0, 'timestamp' => $now];
        }
        
        // Check if we've exceeded the limit
        if ($data['count'] >= $maxQueries) {
            return false;
        }
        
        // Increment counter and save
        $data['count']++;
        @file_put_contents($rateLimitFile, json_encode($data), LOCK_EX);
        
        return true;
    } catch (Exception $e) {
        // On error, allow the query (fail open)
        return true;
    }
}

/**
 * Get country and state/region from IP address using ip-api.com (free tier)
 * Returns array with 'country' and 'state' keys
 * Note: For production with high volume, consider using a local GeoIP database
 * Rate limited to 10 queries per 5 minutes to prevent hanging
 */
function get_location_from_ip(string $ip): array {
    $result = ['country' => '', 'state' => ''];
    
    // Skip for local/private IPs
    if ($ip === '' || $ip === '::1' || strpos($ip, '127.') === 0 || strpos($ip, '192.168.') === 0 || strpos($ip, '10.') === 0) {
        return $result;
    }
    
    // Check rate limit before making API call
    if (!can_query_geolocation()) {
        // Rate limit exceeded, return empty result to prevent hanging
        return $result;
    }
    
    try {
        // Use ip-api.com free API (limit: 45 requests per minute)
        // For production, consider caching results or using a local GeoIP database
        $url = "http://ip-api.com/json/{$ip}?fields=status,country,regionName";
        
        $context = stream_context_create([
            'http' => [
                'timeout' => 2, // 2 second timeout
                'ignore_errors' => true
            ]
        ]);
        
        $response = @file_get_contents($url, false, $context);
        
        if ($response !== false) {
            $data = json_decode($response, true);
            if (isset($data['status']) && $data['status'] === 'success') {
                $result['country'] = $data['country'] ?? '';
                $result['state'] = $data['regionName'] ?? '';
            }
        }
    } catch (Exception $e) {
        // Silently fail - geolocation is not critical
    }
    
    return $result;
}

/**
 * Detect device type from User Agent string
 * Returns one of: 'Mobile', 'Tablet', 'Desktop', or 'Bot'
 */
function get_device_type(string $userAgent): string {
    if ($userAgent === '') {
        return 'Unknown';
    }
    
    $ua = strtolower($userAgent);
    
    // Check for bots/crawlers first
    if (preg_match('/(bot|crawler|spider|slurp|mediapartners)/i', $userAgent)) {
        return 'Bot';
    }
    
    // Check for tablets (before mobile, as many tablets contain "mobile" in UA)
    if (preg_match('/(ipad|tablet|playbook|silk|kindle)/i', $userAgent)) {
        return 'Tablet';
    }
    
    // Check for mobile devices
    if (preg_match('/(mobile|iphone|ipod|android.*mobile|windows phone|blackberry|opera mini)/i', $userAgent)) {
        return 'Mobile';
    }
    
    // Check for smart TVs and gaming consoles
    if (preg_match('/(smart-tv|smarttv|googletv|appletv|hbbtv|pov_tv|netcast|xbox|playstation|nintendo)/i', $userAgent)) {
        return 'Smart TV';
    }
    
    // Default to desktop
    return 'Desktop';
}

/**
 * Try to execute a background command using several strategies.
 * Returns true if a background launch was attempted and likely succeeded.
 */
function try_background_exec(string $cmd): bool {
    $cmd = trim($cmd);
    if ($cmd === '') return false;

    // Windows: try start /B
    if (stripos(PHP_OS, 'WIN') === 0) {
        // Note: Windows background execution is less reliable here; we attempt it.
        $winCmd = "start /B " . $cmd;
        @pclose(@popen($winCmd, "r"));
        return true;
    }

    // Prefer proc_open if available
    if (function_exists('proc_open')) {
        $descriptors = [
            0 => ["pipe", "r"],
            1 => ["file", "/dev/null", "a"],
            2 => ["file", "/dev/null", "a"],
        ];
        $process = @proc_open($cmd, $descriptors, $pipes);
        if (is_resource($process)) {
            @proc_close($process);
            return true;
        }
    }

    // Try exec with async redirection
    if (function_exists('exec')) {
        @exec($cmd . " > /dev/null 2>&1 &");
        return true;
    }

    // Try shell_exec
    if (function_exists('shell_exec')) {
        @shell_exec($cmd . " > /dev/null 2>&1 &");
        return true;
    }

    // Try popen
    if (function_exists('popen')) {
        @pclose(@popen($cmd . " > /dev/null 2>&1 &", "r"));
        return true;
    }

    // No method available
    return false;
}

/**
 * Acquire an operation lock to prevent simultaneous send and fetch operations
 * Uses file-based locking to ensure only one operation type runs at a time
 * 
 * MUTUAL EXCLUSION: Send and fetch operations use the SAME lock file to ensure
 * they cannot run simultaneously. The lock file stores which operation type holds it.
 * 
 * @param string $operation Operation type: 'send' or 'fetch'
 * @param int $timeout Maximum time to wait for lock in seconds
 * @return resource|false Lock file handle on success, false on failure
 */
function acquire_operation_lock(string $operation, int $timeout = 30) {
    $lockDir = sys_get_temp_dir();
    // CRITICAL: Use single lock file for all operations to ensure mutual exclusion
    $lockFile = $lockDir . '/mafia_mailer_operations.lock';
    
    $startTime = time();
    $attempt = 0;
    while (true) {
        // Try to create exclusive lock file
        $fp = @fopen($lockFile, 'c+');
        if ($fp === false) {
            error_log("Failed to open lock file for operation: $operation");
            return false;
        }
        
        // Try to acquire exclusive lock with non-blocking mode first
        if (flock($fp, LOCK_EX | LOCK_NB)) {
            // Write current PID, operation type and timestamp to lock file for debugging
            // Non-critical: log but don't fail if write fails
            try {
                ftruncate($fp, 0);
                $written = fwrite($fp, json_encode([
                    'pid' => getmypid(),
                    'operation' => $operation,
                    'timestamp' => time(),
                    'datetime' => date('Y-m-d H:i:s')
                ]));
                if ($written === false) {
                    error_log("Warning: Failed to write debug info to lock file for $operation");
                }
                fflush($fp);
            } catch (Exception $e) {
                error_log("Warning: Exception writing to lock file for $operation: " . $e->getMessage());
            }
            
            error_log("Operation lock acquired for: $operation (PID: " . getmypid() . ")");
            return $fp;
        }
        
        // Lock is held by another process
        fclose($fp);
        
        // Check timeout
        if (time() - $startTime >= $timeout) {
            error_log("Failed to acquire operation lock for $operation after {$timeout}s timeout");
            return false;
        }
        
        // Exponential backoff: start with 100ms, double up to 2 seconds max
        $delayMs = min(100 * pow(2, $attempt), 2000);
        $attempt++;
        usleep($delayMs * 1000);
    }
}

/**
 * Release an operation lock
 * 
 * @param resource $lockHandle Lock file handle from acquire_operation_lock
 * @param string $operation Operation type for logging
 */
function release_operation_lock($lockHandle, string $operation) {
    if ($lockHandle && is_resource($lockHandle)) {
        flock($lockHandle, LOCK_UN);
        fclose($lockHandle);
        error_log("Operation lock released for: $operation (PID: " . getmypid() . ")");
    }
}

/**
 * Check if an operation lock is currently held (without acquiring it)
 * 
 * Note: Since all operations use the same lock file for mutual exclusion,
 * this checks if ANY operation (send or fetch) is currently locked.
 * 
 * @param string $operation Operation type (for logging only, all use same lock)
 * @return bool True if lock is held, false otherwise
 */
function is_operation_locked(string $operation): bool {
    $lockDir = sys_get_temp_dir();
    // CRITICAL: Use single lock file for all operations (same as acquire_operation_lock)
    $lockFile = $lockDir . '/mafia_mailer_operations.lock';
    
    if (!file_exists($lockFile)) {
        return false;
    }
    
    $fp = @fopen($lockFile, 'c+');
    if ($fp === false) {
        // File exists but can't open - log the issue and assume locked for safety
        error_log("Warning: Lock file exists but cannot open it - assuming locked for safety");
        return true;
    }
    
    // Try to acquire lock in non-blocking mode
    $locked = !flock($fp, LOCK_EX | LOCK_NB);
    
    if (!$locked) {
        // We got the lock, release it immediately
        flock($fp, LOCK_UN);
    }
    
    fclose($fp);
    return $locked;
}

/**
 * Get the optimal temporary directory for recipient storage
 * Prioritizes RAM disk (/dev/shm) for 64GB+ servers to eliminate database bottleneck
 * Falls back to regular temp directory if RAM disk unavailable
 * 
 * @return string Path to temp directory (RAM disk preferred)
 */
function get_recipient_cache_dir(): string {
    if (!USE_RAM_DISK) {
        return sys_get_temp_dir();
    }
    
    // Check if RAM disk is available and writable
    $ramDiskPath = RAM_DISK_PATH;
    if (is_dir($ramDiskPath) && is_writable($ramDiskPath)) {
        // Verify it's actually a tmpfs (RAM-based)
        if (PHP_OS_FAMILY === 'Linux') {
            // Check if it's mounted as tmpfs
            $output = @shell_exec("df -T " . escapeshellarg($ramDiskPath) . " 2>/dev/null | grep tmpfs");
            if ($output !== null && strpos($output, 'tmpfs') !== false) {
                error_log("Using RAM disk for recipient caching: $ramDiskPath (tmpfs)");
                return $ramDiskPath;
            }
        } else {
            // On non-Linux, just check if directory exists and is writable
            if (file_exists($ramDiskPath) && is_writable($ramDiskPath)) {
                error_log("Using RAM disk for recipient caching: $ramDiskPath");
                return $ramDiskPath;
            }
        }
    }
    
    // Fallback to regular temp directory
    if (RAM_DISK_FALLBACK) {
        $tempDir = sys_get_temp_dir();
        error_log("RAM disk not available, using fallback temp directory: $tempDir");
        return $tempDir;
    }
    
    // If no fallback allowed, use RAM disk path anyway (will fail if not available)
    return $ramDiskPath;
}

/**
 * Spawn a detached background PHP process that runs this script with --bg-send
 * Returns true if spawn was likely successful.
 * 
 * REQUIREMENT: Does NOT pass PDO object - workers create their own connections
 * 
 * @param PDO $pdo Database connection (used only for campaign lookup, NOT passed to worker)
 * @param int $campaignId Campaign ID
 * @param string $recipientsText Newline-separated recipient list
 * @param array $overrides Runtime overrides (e.g., send_rate)
 * @return bool True if spawn succeeded
 */
function spawn_background_send(PDO $pdo, int $campaignId, string $recipientsText, array $overrides = []): bool {
    // ENHANCED: Use RAM disk for recipient storage (64GB servers)
    $tmpDir = get_recipient_cache_dir();
    $tmpFile = tempnam($tmpDir, 'ss_send_');
    if ($tmpFile === false) return false;
    $payload = ['recipients' => $recipientsText, 'overrides' => $overrides];
    if (file_put_contents($tmpFile, json_encode($payload)) === false) {
        @unlink($tmpFile);
        return false;
    }

    $php = PHP_BINARY ?: 'php';
    $script = $_SERVER['SCRIPT_FILENAME'] ?? __FILE__;
    // Build command for UNIX-like systems; quoted args
    $cmd = escapeshellcmd($php) . ' -f ' . escapeshellarg($script)
         . ' -- ' . '--bg-send ' . escapeshellarg((string)$campaignId) . ' ' . escapeshellarg($tmpFile);

    // Try variants: with nohup, with & redirect, etc.
    $candidates = [
        $cmd . " > /dev/null 2>&1 &",
        "nohup " . $cmd . " > /dev/null 2>&1 &",
        $cmd, // fallback
    ];

    foreach ($candidates as $c) {
        if (try_background_exec($c)) {
            // the CLI worker will remove the tmpfile after processing; but if spawn failed and we decide to fallback
            return true;
        }
    }

    // if none worked, cleanup and return false (caller will fallback to synchronous send)
    @unlink($tmpFile);
    return false;
}

/**
 * Spawn a detached background PHP process that runs this script with --bg-send-profile <campaignId> <profileId> <tmpfile>
 * Returns true if spawn was likely successful.
 *
 * Accepts $overrides array which will be written to the tmpfile JSON so the CLI worker can apply runtime overrides
 * such as 'send_rate' (messages/sec).
 * 
 * REQUIREMENT: Does NOT pass PDO object - workers create their own connections
 * 
 * @param PDO $pdo Database connection (used only for campaign lookup, NOT passed to worker)
 * @param int $campaignId Campaign ID
 * @param int $profileId Profile ID to use for sending
 * @param string $recipientsText Newline-separated recipient list
 * @param array $overrides Runtime overrides (e.g., send_rate)
 * @return bool True if spawn succeeded
 */
function spawn_background_send_profile(PDO $pdo, int $campaignId, int $profileId, string $recipientsText, array $overrides = []): bool {
    // ENHANCED: Use RAM disk for recipient storage (64GB servers)
    $tmpDir = get_recipient_cache_dir();
    $tmpFile = tempnam($tmpDir, 'ss_send_pf_');
    if ($tmpFile === false) {
        error_log("Campaign $campaignId: FAILED to create temp file in $tmpDir");
        return false;
    }
    
    $payload = ['recipients' => $recipientsText, 'overrides' => $overrides];
    $jsonPayload = json_encode($payload);
    $payloadSize = strlen($jsonPayload);
    
    if (file_put_contents($tmpFile, $jsonPayload) === false) {
        error_log("Campaign $campaignId: FAILED to write $payloadSize bytes to temp file $tmpFile");
        @unlink($tmpFile);
        return false;
    }
    
    // Log for large payloads
    if ($payloadSize > 1000000) { // > 1MB
        error_log("Campaign $campaignId: Created large temp file $tmpFile with " . number_format($payloadSize) . " bytes");
    }

    $php = PHP_BINARY ?: 'php';
    $script = $_SERVER['SCRIPT_FILENAME'] ?? __FILE__;
    $cmd = escapeshellcmd($php) . ' -f ' . escapeshellarg($script)
         . ' -- ' . '--bg-send-profile ' . escapeshellarg((string)$campaignId) . ' ' . escapeshellarg((string)$profileId) . ' ' . escapeshellarg($tmpFile);

    $candidates = [
        $cmd . " > /dev/null 2>&1 &",
        "nohup " . $cmd . " > /dev/null 2>&1 &",
        $cmd,
    ];

    foreach ($candidates as $c) {
        if (try_background_exec($c)) {
            return true;
        }
    }

    error_log("Campaign $campaignId: FAILED to execute background worker command");
    @unlink($tmpFile);
    return false;
}

/**
 * Spawn multiple parallel workers for sending emails
 * 
 * SENDGRID-STYLE BATCH PROCESSING (per requirements):
 * - Pre-loads ALL recipients into memory (no per-email DB queries)
 * - Distributes recipients across workers in-memory before spawning
 * - Workers receive pre-assembled batches via temporary files (RAM disk preferred)
 * - Each worker creates its own DB connection (never passed from parent)
 * - Spawns workers in batches to prevent connection spikes
 * 
 * Spawns the specified number of workers (or fewer if there are fewer recipients than workers)
 * Distributes recipients evenly across workers
 * 
 * REQUIREMENT: Maximum 160 workers enforced (MAX_WORKERS constant)
 * REQUIREMENT: Workers spawned in batches with delays to prevent DB connection exhaustion
 * 
 * @param PDO $pdo Database connection (used for spawning only, NOT passed to workers)
 * @param int $campaignId Campaign ID
 * @param array $recipients Array of recipient email addresses (pre-loaded batch)
 * @param int $workers Number of workers to spawn
 * @param int $messagesPerWorker Messages per worker
 * @param int|null $profileId Optional profile ID to use
 * @param array $overrides Runtime overrides
 * @param int $cycleDelayMs Delay between distribution rounds in milliseconds
 * @return array Status array with success, workers count, and failures
 */
function spawn_parallel_workers(PDO $pdo, int $campaignId, array $recipients, int $workers, int $messagesPerWorker, ?int $profileId = null, array $overrides = [], int $cycleDelayMs = 0): array {
    $totalRecipients = count($recipients);
    error_log("Campaign $campaignId: spawn_parallel_workers called with $totalRecipients recipients, $workers workers requested");
    
    if ($totalRecipients === 0) return ['success' => true, 'workers' => 0];
    
    // Validate workers parameter - enforce both minimum and maximum limits for resource safety
    $workers = max(MIN_WORKERS, min(MAX_WORKERS, $workers));
    
    // Log info for large worker counts (informational only)
    if ($workers >= WORKERS_WARNING_THRESHOLD) {
        error_log("Info: Large number of workers requested ($workers) for campaign $campaignId. High throughput mode with resource safety cap.");
    }
    
    $messagesPerWorker = max(MIN_MESSAGES_PER_WORKER, $messagesPerWorker);
    $cycleDelayMs = max(MIN_CYCLE_DELAY_MS, min(MAX_CYCLE_DELAY_MS, $cycleDelayMs));
    
    // Spawn up to $workers parallel workers (but not more than total recipients)
    $actualWorkers = min($workers, $totalRecipients);
    
    // Distribute recipients across workers in chunks of messagesPerWorker
    // If messagesPerWorker is small, each worker gets multiple small batches in round-robin fashion
    // If messagesPerWorker is large, we may need fewer workers
    
    $spawnedWorkers = 0;
    $failures = [];
    
    // PERFORMANCE: Distribute all recipients across workers in a round-robin fashion
    // Each "round" gives each worker up to messagesPerWorker emails
    $workerRecipients = array_fill(0, $actualWorkers, []);
    
    $recipientIdx = 0;
    $roundCount = 0;
    while ($recipientIdx < $totalRecipients) {
        // OPTIMIZATION: Apply cycle delay between rounds (except for the first round)
        // With cycleDelayMs=0 (default), this has no impact - maximum speed
        if ($roundCount > 0 && $cycleDelayMs > 0) {
            usleep($cycleDelayMs * 1000); // Convert milliseconds to microseconds
        }
        
        for ($workerIdx = 0; $workerIdx < $actualWorkers && $recipientIdx < $totalRecipients; $workerIdx++) {
            // Give this worker up to messagesPerWorker emails in this round
            $chunkSize = min($messagesPerWorker, $totalRecipients - $recipientIdx);
            $chunk = array_slice($recipients, $recipientIdx, $chunkSize);
            // Use array_push with spread operator for O(n) performance instead of array_merge O(n²)
            array_push($workerRecipients[$workerIdx], ...$chunk);
            $recipientIdx += $chunkSize;
        }
        
        $roundCount++;
    }
    
    // OPTIMIZATION: Spawn workers in batches to prevent "too many connections" errors
    // Instead of spawning all 5000 workers at once, spawn them in batches of MAX_CONCURRENT_WORKERS
    // This keeps database connections under control while maintaining high throughput
    $batchSize = MAX_CONCURRENT_WORKERS;
    $totalWorkerBatches = array_chunk($workerRecipients, $batchSize, true);
    
    // CRITICAL LOGGING: Show email distribution per worker to debug 75,500 issue
    $totalAssigned = 0;
    $nonEmptyWorkers = 0;
    foreach ($workerRecipients as $idx => $recips) {
        $count = count($recips);
        $totalAssigned += $count;
        if ($count > 0) $nonEmptyWorkers++;
        if ($idx < 3 || $count != count($workerRecipients[0])) {
            error_log("Campaign $campaignId: Worker $idx assigned $count recipients");
        }
    }
    error_log("Campaign $campaignId DISTRIBUTION: $nonEmptyWorkers workers assigned $totalAssigned total recipients (should be $totalRecipients)");
    
    error_log("Campaign $campaignId: Spawning $actualWorkers workers in " . count($totalWorkerBatches) . " batches of up to $batchSize workers each");
    
    foreach ($totalWorkerBatches as $batchIdx => $workerBatch) {
        // Spawn this batch of workers
        foreach ($workerBatch as $workerIdx => $recips) {
            if (empty($recips)) continue;
            
            $chunkText = implode("\n", $recips);
            
            if ($profileId !== null) {
                $spawned = spawn_background_send_profile($pdo, $campaignId, $profileId, $chunkText, $overrides);
            } else {
                $spawned = spawn_background_send($pdo, $campaignId, $chunkText, $overrides);
            }
            
            if ($spawned) {
                $spawnedWorkers++;
            } else {
                error_log("Campaign $campaignId: FAILED to spawn worker for batch $batchIdx, worker $workerIdx with " . count($recips) . " recipients");
                $failures[] = ['chunk' => $recips, 'profileId' => $profileId, 'overrides' => $overrides];
            }
        }
        
        // Add delay between batches (except after the last batch)
        // This allows earlier workers to establish connections and start working
        // before the next batch tries to connect, preventing connection spikes
        if ($batchIdx < count($totalWorkerBatches) - 1 && WORKER_SPAWN_BATCH_DELAY_MS > 0) {
            usleep(WORKER_SPAWN_BATCH_DELAY_MS * 1000); // Convert milliseconds to microseconds
            error_log("Campaign $campaignId: Spawned batch " . ($batchIdx + 1) . "/" . count($totalWorkerBatches) . " ($spawnedWorkers workers so far), waiting " . WORKER_SPAWN_BATCH_DELAY_MS . "ms before next batch");
        }
    }
    
    // Log final spawn summary
    $failureCount = count($failures);
    if ($failureCount > 0) {
        error_log("Campaign $campaignId: WARNING - $failureCount worker spawn failures out of " . count($workerRecipients) . " attempted");
    }
    error_log("Campaign $campaignId: Successfully spawned $spawnedWorkers workers for $totalRecipients recipients");
    
    return [
        'success' => empty($failures),
        'workers' => $spawnedWorkers,
        'failures' => $failures
    ];
}

/**
 * Spawn a detached background PHP process to scan IMAP bounce mailboxes
 */
function spawn_bounce_scan(PDO $pdo): bool {
    // OPTIMIZATION: Quick check before spawning background process
    // While the lock system ensures mutual exclusion, this check prevents
    // unnecessary process spawning when send is already running
    if (is_operation_locked('send')) {
        error_log("Bounce scan skipped: Send operation is currently running (pre-spawn check)");
        return false;
    }
    
    $php = PHP_BINARY ?: 'php';
    $script = $_SERVER['SCRIPT_FILENAME'] ?? __FILE__;
    $cmd = escapeshellcmd($php) . ' -f ' . escapeshellarg($script)
         . ' -- ' . '--bg-scan-bounces';

    $candidates = [
        $cmd . " > /dev/null 2>&1 &",
        "nohup " . $cmd . " > /dev/null 2>&1 &",
        $cmd,
    ];

    foreach ($candidates as $c) {
        if (try_background_exec($c)) {
            return true;
        }
    }
    return false;
}

/**
 * Get a global setting value
 */
function get_setting(PDO $pdo, string $key, string $default = ''): string {
    static $cache = [];
    if (array_key_exists($key, $cache)) {
        return $cache[$key];
    }
    try {
        $stmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = ?");
        $stmt->execute([$key]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        $cache[$key] = $result ? $result['setting_value'] : $default;
        return $cache[$key];
    } catch (Exception $e) {
        return $default;
    }
}

/**
 * Set a global setting value
 */
function set_setting(PDO $pdo, string $key, string $value): bool {
    try {
        $stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = ?, updated_at = CURRENT_TIMESTAMP");
        $stmt->execute([$key, $value, $value]);
        return true;
    } catch (Exception $e) {
        return false;
    }
}

/**
 * Resolve the messages-per-worker value for a profile.
 *
 * The DB column may hold a stale/old value (e.g. 1000 or 2000 from before the 500
 * default was set).  Clamping it to DEFAULT_MESSAGES_PER_WORKER prevents the
 * round-robin distribution from filling fewer worker slots than were allocated.
 *
 * @param int|null $dbValue Raw value from the profile/settings row (may be null)
 * @return int Effective messages-per-worker, in [MIN_MESSAGES_PER_WORKER, DEFAULT_MESSAGES_PER_WORKER]
 */
function resolve_messages_per_worker(?int $dbValue): int {
    $raw = $dbValue ?? DEFAULT_MESSAGES_PER_WORKER;
    return max(MIN_MESSAGES_PER_WORKER, min(DEFAULT_MESSAGES_PER_WORKER, $raw));
}

/**
 * Calculate optimal number of workers based on email count
 * Formula: 1 worker per DEFAULT_MESSAGES_PER_WORKER emails (integer / floor division)
 * Example: 21,357 emails / 500 per worker = 42 workers
 * In rotation mode each profile receives a share of the total, so the formula
 * naturally scales: e.g. 2 profiles × 10,679 emails each → 21 workers each = 42 total.
 * 
 * @param int $emailCount Total number of emails to send
 * @return int Number of workers to use (minimum MIN_WORKERS, maximum MAX_WORKERS)
 */
function calculate_workers_from_count(int $emailCount): int {
    if ($emailCount <= 0) {
        return MIN_WORKERS;
    }
    
    // One worker per DEFAULT_MESSAGES_PER_WORKER emails (floor division).
    // For 21,357 emails with 500/worker → intdiv(21357, 500) = 42 workers.
    // When emailCount < DEFAULT_MESSAGES_PER_WORKER (e.g. 300 emails), intdiv returns 0;
    // the max(MIN_WORKERS, ...) guard below ensures at least 1 worker in that case.
    // Last worker batch may receive fewer than DEFAULT_MESSAGES_PER_WORKER emails.
    $divisor = max(1, DEFAULT_MESSAGES_PER_WORKER); // guard against misconfigured zero
    $workers = max(MIN_WORKERS, intdiv($emailCount, $divisor));
    
    // Enforce maximum limit for resource safety
    $workers = min(MAX_WORKERS, $workers);
    
    // Log info for large worker counts (informational only)
    if ($workers >= WORKERS_WARNING_THRESHOLD && $emailCount >= 10000) {
        error_log("Auto-calculated worker count: $workers for $emailCount emails (capped at MAX_WORKERS=" . MAX_WORKERS . " for resource safety)");
    }
    
    return $workers;
}

/**
 * Wait for worker processes to complete with timeout and non-blocking polling
 * 
 * Uses WNOHANG flag with pcntl_waitpid to prevent blocking indefinitely on worker processes.
 * Polls worker status every 100ms (WORKER_POLL_INTERVAL_US) until all workers complete or timeout is reached.
 * This prevents the main process from hanging if workers fail to exit properly.
 * 
 * @param array $workerPids Array of worker process IDs to wait for
 * @param int $maxWaitTime Maximum time to wait in seconds (default: WORKER_WAIT_TIMEOUT_SECONDS)
 * @return void
 */
function wait_for_workers(array $workerPids, int $maxWaitTime = WORKER_WAIT_TIMEOUT_SECONDS): void {
    if (empty($workerPids)) {
        return;
    }
    
    $startTime = time();
    $remainingPids = $workerPids;
    
    while (!empty($remainingPids) && (time() - $startTime) < $maxWaitTime) {
        foreach ($remainingPids as $key => $pid) {
            $status = 0;
            // Use WNOHANG to avoid blocking indefinitely
            $result = pcntl_waitpid($pid, $status, WNOHANG);
            if ($result == $pid) {
                // Process has exited
                unset($remainingPids[$key]);
            } elseif ($result == -1) {
                // Error or process doesn't exist
                unset($remainingPids[$key]);
            }
            // If $result == 0, process is still running
        }
        if (!empty($remainingPids)) {
            usleep(WORKER_POLL_INTERVAL_US); // Sleep 100ms before checking again
        }
    }
    
    // Clean up any remaining zombie processes
    // Only attempt cleanup if process still exists
    foreach ($remainingPids as $pid) {
        $status = 0;
        $result = pcntl_waitpid($pid, $status, WNOHANG);
        // Result will be -1 if process doesn't exist, 0 if still running, or PID if exited
        // We don't need to do anything with the result here, just reap any zombies
    }
}

/**
 * Escape special characters in LIKE pattern to prevent SQL injection
 * 
 * @param string $pattern The pattern to escape
 * @return string The escaped pattern
 */
function escape_like_pattern(string $pattern): string {
    // Escape LIKE wildcards and escape character
    $pattern = str_replace('\\', '\\\\', $pattern); // Escape the escape character first
    $pattern = str_replace('%', '\\%', $pattern);
    $pattern = str_replace('_', '\\_', $pattern);
    return $pattern;
}

/**
 * Inject open & click tracking inside HTML
 */
function build_tracked_html(array $campaign, string $recipientEmail = ''): string
{
    global $pdo;
    
    $html = $campaign['html'] ?? '';
    if ($html === '') {
        return '';
    }

    $cid  = (int)$campaign['id'];
    $base = get_base_url();

    $rParam = '';
    if ($recipientEmail !== '') {
        $rParam = '&r=' . rawurlencode(base64url_encode(strtolower($recipientEmail)));
    }

    $unsubscribeEnabled = !empty($campaign['unsubscribe_enabled']) ? true : false;
    
    // Use global tracking settings
    $openTrackingEnabled = (get_setting($pdo, 'open_tracking_enabled', '1') === '1');
    $clickTrackingEnabled = (get_setting($pdo, 'click_tracking_enabled', '1') === '1');

    // Only inject open tracking if enabled
    // Use CSS with inline base64 image to avoid spam filters
    if ($openTrackingEnabled) {
        $openUrl = $base . '?t=open&cid=' . $cid . $rParam;
        // Use CSS with a transparent 1x1 GIF as background - less likely to be flagged as spam
        // Also add a fallback IMG tag for email clients that don't support CSS backgrounds
        $pixel   = '<div style="width:0;height:0;line-height:0;font-size:0;background:transparent url(\'' . $openUrl . '\') no-repeat center;"></div>';

        if (stripos($html, '</body>') !== false) {
            $html = preg_replace('~</body>~i', $pixel . '</body>', $html, 1);
        } else {
            $html .= $pixel;
        }
    }

    $pattern_unsub = '~<a\b([^>]*?)\bhref\s*=\s*(["\'])(.*?)\2([^>]*)>(.*?)</a>~is';
    $foundUnsubAnchor = false;
    $html = preg_replace_callback($pattern_unsub, function ($m) use ($base, $cid, $rParam, $unsubscribeEnabled, &$foundUnsubAnchor) {
        $beforeAttrs = $m[1];
        $quote       = $m[2];
        $href        = $m[3];
        $afterAttrs  = $m[4];
        $innerText   = $m[5];

        if (stripos($innerText, 'unsubscribe') !== false) {
            if (!$unsubscribeEnabled) {
                return '';
            }
            $foundUnsubAnchor = true;
            $unsubUrl = $base . '?t=unsubscribe&cid=' . $cid . $rParam;
            return '<a' . $beforeAttrs . ' href=' . $quote . $unsubUrl . $quote . $afterAttrs . '>' . $innerText . '</a>';
        }
        return $m[0];
    }, $html);

    if ($unsubscribeEnabled && !$foundUnsubAnchor) {
        $unsubUrl = $base . '?t=unsubscribe&cid=' . $cid . $rParam;
        $unsubBlock = '<div style="text-align:center;margin-top:18px;color:#777;font-size:13px;">'
                     . '<a href="' . $unsubUrl . '" style="color:#1A82E2;">Unsubscribe</a>'
                     . '</div>';
        if (stripos($html, '</body>') !== false) {
            $html = preg_replace('~</body>~i', $unsubBlock . '</body>', $html, 1);
        } else {
            $html .= $unsubBlock;
        }
    }

    // Only wrap links with click tracking if enabled
    if ($clickTrackingEnabled) {
        $pattern = '~<a\b([^>]*?)\bhref\s*=\s*(["\'])(.*?)\2([^>]*)>~i';

        $html = preg_replace_callback($pattern, function ($m) use ($base, $cid, $rParam) {
            $beforeAttrs = $m[1];
            $quote       = $m[2];
            $href        = $m[3];
            $afterAttrs  = $m[4];

            $trimHref = trim($href);

            if (
                $trimHref === '' ||
                stripos($trimHref, 'mailto:') === 0 ||
                stripos($trimHref, 'javascript:') === 0 ||
                $trimHref[0] === '#'
            ) {
                return $m[0];
            }

            if (stripos($trimHref, '?t=click') !== false && stripos($trimHref, 'cid=') !== false) {
                return $m[0];
            }
            if (stripos($trimHref, '?t=unsubscribe') !== false && stripos($trimHref, 'cid=') !== false) {
                return $m[0];
            }

            // Check if href contains template tags (e.g., {{email}}, {{name}}, etc.)
            // If it does, preserve them by not encoding the URL yet
            if (preg_match('/\{\{[^}]+\}\}/', $trimHref)) {
                // URL contains template tags - store it without encoding to preserve tags
                // We'll use a special marker to indicate this URL needs tag preservation
                $encodedUrl = base64url_encode($trimHref);
                $trackUrl   = $base . '?t=click&cid=' . $cid . '&u=' . rawurlencode($encodedUrl) . $rParam;
            } else {
                // Normal URL without template tags - encode as usual
                $encodedUrl = base64url_encode($trimHref);
                $trackUrl   = $base . '?t=click&cid=' . $cid . '&u=' . rawurlencode($encodedUrl) . $rParam;
            }

            return '<a' . $beforeAttrs . ' href=' . $quote . $trackUrl . $quote . $afterAttrs . '>';
        }, $html);
    }

    return $html;
}

/**
 * Apply proxy settings to a cURL handle based on a sending profile.
 * Does nothing if proxy_enabled is not set.
 */
function apply_proxy_to_curl($ch, array $profile): void {
    if (empty($profile['proxy_enabled'])) {
        return;
    }
    $host = trim($profile['proxy_host'] ?? '');
    if ($host === '') {
        return;
    }
    $port = (int)($profile['proxy_port'] ?? 8080);
    $type = strtolower(trim($profile['proxy_type'] ?? 'http'));

    curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port);
    if ($type === 'socks5') {
        curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
    } elseif ($type === 'socks4') {
        curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS4);
    } else {
        curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
    }
    $user = trim($profile['proxy_user'] ?? '');
    $pass = (string)($profile['proxy_pass'] ?? '');
    if ($user !== '') {
        curl_setopt($ch, CURLOPT_PROXYUSERPWD, $user . ':' . $pass);
    }
}

function smtp_check_connection(array $profile): array {
    $log = [];

    $host = trim($profile['host'] ?? '');
    $port = (int)($profile['port'] ?? 587);
    $user = trim($profile['username'] ?? '');
    $pass = (string)($profile['password'] ?? '');

    if ($host === '') {
        return ['ok'=>false,'msg'=>'Missing SMTP host','log'=>$log];
    }

    $remoteHost = ($port === 465 ? "ssl://{$host}" : $host);

    $errno = 0;
    $errstr = '';
    $socket = @fsockopen($remoteHost, $port, $errno, $errstr, 10);
    if (!$socket) {
        $log[] = "connect_error: {$errno} {$errstr}";
        return ['ok'=>false,'msg'=>"SMTP connect error: {$errno} {$errstr}", 'log'=>$log];
    }
    stream_set_timeout($socket, 10);

    $read = function() use ($socket, &$log) {
        $data = '';
        while ($str = fgets($socket, 515)) {
            $data .= $str;
            $log[] = rtrim($str, "\r\n");
            if (strlen($str) < 4) break;
            if (substr($str, 3, 1) === ' ') break;
        }
        $code = isset($data[0]) ? substr($data, 0, 3) : null;
        $msg  = isset($data[4]) ? trim(substr($data, 4)) : trim($data);
        return [$code, $msg, $data];
    };
    $write = function(string $cmd) use ($socket, $read, &$log) {
        fputs($socket, $cmd . "\r\n");
        $log[] = 'C: ' . $cmd;
        return $read();
    };

    list($gcode, $gmsg) = $read();
    if (!is_string($gcode) || substr($gcode,0,1) !== '2') {
        fclose($socket);
        $log[] = 'banner_failed';
        return ['ok'=>false,'msg'=>"SMTP banner failed: {$gcode} {$gmsg}", 'log'=>$log];
    }

    list($ecode, $emsg) = $write('EHLO ' . smtp_get_ehlo_hostname($user));
    if (!is_string($ecode) || substr($ecode,0,1) !== '2') {
        list($hcode, $hmsg) = $write('HELO ' . smtp_get_ehlo_hostname($user));
        if (!is_string($hcode) || substr($hcode,0,1) !== '2') {
            fclose($socket);
            $log[] = 'ehlo_failed';
            return ['ok'=>false,'msg'=>"EHLO/HELO failed: {$ecode} / {$hcode}", 'log'=>$log];
        } else {
            $ecode = $hcode; $emsg = $hmsg;
        }
    }

    if ($user !== '') {
        list($acode, $amsg) = $write('AUTH LOGIN');
        if (!is_string($acode) || substr($acode,0,1) !== '3') {
            fclose($socket);
            return ['ok'=>true,'msg'=>'Connected; AUTH not required/accepted','log'=>$log];
        }
        list($ucode, $umsg) = $write(base64_encode($user));
        if (!is_string($ucode) || substr($ucode,0,1) !== '3') {
            fclose($socket);
            return ['ok'=>false,'msg'=>"AUTH username rejected: {$ucode} {$umsg}", 'log'=>$log];
        }
        list($pcode, $pmsg) = $write(base64_encode($pass));
        if (!is_string($pcode) || substr($pcode,0,1) !== '2') {
            fclose($socket);
            return ['ok'=>false,'msg'=>"AUTH password rejected: {$pcode} {$pmsg}", 'log'=>$log];
        }
    }

    $write('QUIT');
    fclose($socket);

    return ['ok'=>true,'msg'=>'SMTP connect OK','log'=>$log];
}

function api_check_connection(array $profile): array {
    $apiUrl = trim($profile['api_url'] ?? '');
    $apiKey = trim($profile['api_key'] ?? '');
    $provider = trim($profile['provider'] ?? '');

    $log = [];

    if ($apiUrl === '') {
        return ['ok'=>false,'msg'=>'Missing API URL','log'=>$log];
    }

    $log[] = 'Testing connection to: ' . $apiUrl;
    $log[] = 'Provider: ' . ($provider ?: 'Generic');

    // Prepare headers
    $headers = ['Content-Type: application/json'];
    if ($apiKey !== '') {
        $headers[] = 'Authorization: Bearer ' . $apiKey;
        $log[] = 'Using API key: ' . substr($apiKey, 0, 10) . '...';
    } else {
        $log[] = 'No API key provided';
    }
    
    if (!empty($profile['headers_json'])) {
        $extra = json_decode($profile['headers_json'], true);
        if (is_array($extra)) {
            foreach ($extra as $k => $v) {
                $headers[] = "{$k}: {$v}";
                $log[] = 'Custom header: ' . $k;
            }
        }
    }

    // Build provider-specific test payload
    $testCampaign = [
        'subject' => 'Connection Test',
        'sender_name' => 'Test Sender'
    ];
    $testPayload = api_build_payload($provider, 'test@example.com', 'test@example.com', $testCampaign, '<p>Test</p>');
    
    if ($testPayload === null) {
        $testPayload = ['test' => 'connection'];
    }

    $log[] = 'Attempting POST request with provider-specific payload...';

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $apiUrl);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($testPayload));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 15);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    apply_proxy_to_curl($ch, $profile);
    
    // Handle SSL certificate issues (common with APIs)
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
    
    // Get verbose information
    curl_setopt($ch, CURLOPT_VERBOSE, false);

    $resp = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curlInfo = curl_getinfo($ch);
    
    if ($resp === false) {
        $err = curl_error($ch);
        $errNo = curl_errno($ch);
        curl_close($ch);
        $log[] = 'cURL error #' . $errNo . ': ' . $err;
        
        // If SSL error, try without verification (fallback)
        if ($errNo === 60 || $errNo === 77) {
            $log[] = 'SSL verification failed, retrying without SSL verification...';
            
            $ch2 = curl_init();
            curl_setopt($ch2, CURLOPT_URL, $apiUrl);
            curl_setopt($ch2, CURLOPT_POST, true);
            curl_setopt($ch2, CURLOPT_POSTFIELDS, $testPayload);
            curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch2, CURLOPT_TIMEOUT, 15);
            curl_setopt($ch2, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($ch2, CURLOPT_MAXREDIRS, 3);
            curl_setopt($ch2, CURLOPT_HTTPHEADER, $headers);
            curl_setopt($ch2, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch2, CURLOPT_SSL_VERIFYHOST, 0);
            apply_proxy_to_curl($ch2, $profile);
            
            $resp = curl_exec($ch2);
            $code = curl_getinfo($ch2, CURLINFO_HTTP_CODE);
            
            if ($resp === false) {
                $err2 = curl_error($ch2);
                curl_close($ch2);
                $log[] = 'Retry also failed: ' . $err2;
                return ['ok'=>false,'msg'=>"cURL error: {$err}", 'log'=>$log];
            }
            curl_close($ch2);
            $log[] = 'SSL verification bypass succeeded';
        } else {
            return ['ok'=>false,'msg'=>"cURL error: {$err}", 'log'=>$log];
        }
    } else {
        curl_close($ch);
    }

    $log[] = 'HTTP CODE: ' . $code;
    $log[] = 'Response preview: ' . substr($resp, 0, 150) . (strlen($resp) > 150 ? '...' : '');
    $log[] = 'Effective URL: ' . ($curlInfo['url'] ?? $apiUrl);
    
    // Very permissive acceptance criteria for connection testing
    // The goal is to verify the endpoint exists and is reachable, not to validate the full request
    if ($code >= 200 && $code < 300) {
        // 2xx - perfect success
        $log[] = 'Success: API accepted the request';
        return ['ok'=>true,'msg'=>'API connection successful (HTTP ' . $code . ')','log'=>$log];
    } elseif ($code >= 300 && $code < 400) {
        // 3xx - redirect is acceptable
        $log[] = 'Success: API endpoint found (redirect)';
        return ['ok'=>true,'msg'=>'API endpoint found (HTTP ' . $code . ' redirect)','log'=>$log];
    } elseif ($code === 400 || $code === 401 || $code === 403 || $code === 422) {
        // These codes mean endpoint exists but rejected our test request - this is GOOD
        $log[] = 'Success: Endpoint exists and responded (rejected test data as expected)';
        return ['ok'=>true,'msg'=>'API endpoint is reachable and working (HTTP ' . $code . ')','log'=>$log];
    } elseif ($code === 405) {
        // Method not allowed - the endpoint exists but doesn't accept POST
        // Try GET as fallback
        $log[] = 'POST not allowed, trying GET method...';
        
        $ch3 = curl_init();
        curl_setopt($ch3, CURLOPT_URL, $apiUrl);
        curl_setopt($ch3, CURLOPT_HTTPGET, true);
        curl_setopt($ch3, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch3, CURLOPT_TIMEOUT, 10);
        curl_setopt($ch3, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch3, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch3, CURLOPT_SSL_VERIFYHOST, 0);
        apply_proxy_to_curl($ch3, $profile);
        
        $resp3 = curl_exec($ch3);
        $code3 = curl_getinfo($ch3, CURLINFO_HTTP_CODE);
        curl_close($ch3);
        
        $log[] = 'GET method returned HTTP ' . $code3;
        
        if ($code3 >= 200 && $code3 < 500 && $code3 !== 404) {
            return ['ok'=>true,'msg'=>'API endpoint is reachable (HTTP ' . $code3 . ' via GET)','log'=>$log];
        }
    } elseif ($code === 404) {
        // 404 - this usually means the URL path is wrong
        $log[] = 'Error: 404 means the endpoint URL path is incorrect';
        $log[] = 'Please verify the complete API URL including the path (e.g., /v1/send)';
        return ['ok'=>false,'msg'=>'API endpoint not found (HTTP 404). Verify the complete URL including path.','log'=>$log];
    } elseif ($code >= 500) {
        // 5xx server error - endpoint exists but server has issues
        $log[] = 'Warning: Server error - endpoint exists but API server has problems';
        return ['ok'=>false,'msg'=>'API server error (HTTP ' . $code . '). The endpoint exists but the server has issues.','log'=>$log];
    } else {
        // Other codes
        $log[] = 'Unexpected HTTP code: ' . $code;
        return ['ok'=>false,'msg'=>'API returned HTTP ' . $code,'log'=>$log];
    }
    
    return ['ok'=>false,'msg'=>'API check completed with HTTP ' . $code,'log'=>$log];
}

/**
 * Convert HTML to plain text for email
 */
function html_to_plain_text(string $html): string
{
    // Remove script and style tags and their content
    $text = preg_replace('/<script[^>]*?>.*?<\/script>/si', '', $html);
    $text = preg_replace('/<style[^>]*?>.*?<\/style>/si', '', $text);
    
    // Extract alt text from images (contributes to word count for spam filters)
    $text = preg_replace_callback('/<img[^>]+alt=["\']([^"\']+)["\'][^>]*>/i', function($m){
        $alt = strip_tags(html_entity_decode($m[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'));
        return ' [' . $alt . '] ';
    }, $text);
    // Drop img tags with no alt
    $text = preg_replace('/<img[^>]*>/i', '', $text);
    
    // Convert common HTML elements to text equivalents
    $text = preg_replace('/<br\s*\/?>/i', "\n", $text);
    $text = preg_replace('/<\/p>/i', "\n\n", $text);
    $text = preg_replace('/<\/div>/i', "\n", $text);
    $text = preg_replace('/<\/h[1-6]>/i', "\n\n", $text);
    $text = preg_replace('/<li>/i', "\n• ", $text);
    $text = preg_replace('/<\/li>/i', "", $text);
    $text = preg_replace('/<hr\s*\/?>/i', "\n----------------------------------------\n", $text);
    
    // Convert links to text with URL
    $text = preg_replace('/<a\s+href=["\']([^"\']+)["\'][^>]*>([^<]+)<\/a>/i', '$2 ($1)', $text);
    
    // Strip remaining HTML tags
    $text = strip_tags($text);
    
    // Decode HTML entities
    $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    
    // Clean up whitespace
    $text = preg_replace('/\n\s*\n\s*\n/', "\n\n", $text); // Max 2 consecutive newlines
    $text = preg_replace('/[ \t]+/', ' ', $text); // Collapse spaces
    $text = trim($text);
    
    return $text;
}

/**
 * Ensure HTML content is properly wrapped in HTML tags
 */
function ensure_html_wrapper(string $html): string
{
    // Check if already has html tag
    if (preg_match('/<html[^>]*>/i', $html)) {
        return $html;
    }
    
    // Wrap in proper HTML structure with good default styling
    $wrapped = "<!DOCTYPE html>\n";
    $wrapped .= "<html>\n";
    $wrapped .= "<head>\n";
    $wrapped .= "<meta charset=\"UTF-8\">\n";
    $wrapped .= "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n";
    $wrapped .= "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n";
    $wrapped .= "<style type=\"text/css\">\n";
    $wrapped .= "  body { margin: 0; padding: 0; font-family: Arial, sans-serif; font-size: 14px; line-height: 1.6; color: #333333; background-color: #ffffff; }\n";
    $wrapped .= "  a { color: #0066cc; text-decoration: underline; }\n";
    $wrapped .= "  a:hover { color: #004499; }\n";
    $wrapped .= "  img { border: 0; display: block; }\n";
    $wrapped .= "  table { border-collapse: collapse; }\n";
    // Prevent low-contrast / near-invisible text that triggers HTML_FONT_LOW_CONTRAST
    $wrapped .= "  [style*='color:#fff'],[style*='color: #fff'],[style*='color:white'],[style*='color: white'],[style*='color:#ffffff'],[style*='color: #ffffff'] { color: #333333 !important; }\n";
    $wrapped .= "  [style*='font-size:0'],[style*='font-size: 0'] { font-size: 14px !important; }\n";
    $wrapped .= "</style>\n";
    $wrapped .= "</head>\n";
    $wrapped .= "<body style=\"margin: 0; padding: 20px; font-family: Arial, sans-serif; color: #333333; background-color: #ffffff;\">\n";
    $wrapped .= $html;
    $wrapped .= "\n</body>\n";
    $wrapped .= "</html>";
    
    return $wrapped;
}

/**
 * Generate a unique Message-ID for email
 */
function generate_message_id(string $domain): string
{
    // Extract domain from email if full email provided
    if (strpos($domain, '@') !== false) {
        $parts = explode('@', $domain);
        $domain = end($parts);
    }
    
    // Generate unique ID: timestamp.random.pid@domain
    $uniqueId = time() . '.' . bin2hex(random_bytes(8)) . '.' . getmypid();
    return "<{$uniqueId}@{$domain}>";
}

/**
 * Derive a proper EHLO/HELO hostname from the sender's email address.
 * Using the actual sending domain prevents the SPF_HELO_NONE SpamAssassin rule.
 */
function smtp_get_ehlo_hostname(string $emailOrUsername): string
{
    $atPos = strpos($emailOrUsername, '@');
    if ($atPos !== false) {
        $domain = trim(substr($emailOrUsername, $atPos + 1));
        if ($domain !== '') {
            return $domain;
        }
    }
    $h = gethostname();
    return ($h !== false && $h !== '') ? $h : 'mail.local';
}

/**
 * Inject a unique per-recipient identity token into the email body.
 *
 * Razor2 bypass strategy:
 *  1. Scatter markers (invisible `<span style="display:none">` with unique
 *     3-char words) are injected after every 4th block-level closing tag.
 *     After HTML-tag stripping (Razor2 normalization), each word appears in
 *     the normalized text, ensuring no 64-byte window in the campaign body
 *     is identical across sends.
 *  2. The visible tailDiv at the end of the body carries a cryptographically
 *     random 16-char hex $uniqueRef plus the recipient address and a
 *     microsecond-precise timestamp.  These three values guarantee that every
 *     email produces a different Razor2 full-body hash (Engine 4).
 *  3. No visible "boilerplate" paragraph is added — the recipient's intended
 *     email content is preserved exactly as authored.
 */
function inject_email_token(string $html, string $recipientEmail, array $campaign = []): string
{
    $safeEmail = htmlspecialchars($recipientEmail, ENT_QUOTES, 'UTF-8');

    // Microsecond-precise timestamp — unique to every individual send
    $now             = microtime(true);
    $microsecondsStr = sprintf('%06d', (int)(fmod($now, 1) * 1000000));

    // Cryptographically random 16-char hex token — different for every call.
    $uniqueRef = bin2hex(random_bytes(8));

    // Scatter unique 3-letter markers after every 4th block-level closing tag.
    // These spans are hidden from the reader (display:none / aria-hidden) but
    // appear in Razor2's HTML-stripped normalized text, breaking up any static
    // 64-byte window that might otherwise match a stored spam signature.
    $vowels         = 'aeiou';
    $consonants     = 'bcdfghjklmnpqrstvwxyz';
    $consonantCount = strlen($consonants) - 1; // 20, used as max index
    $vowelCount     = strlen($vowels) - 1;     // 4, used as max index
    $scatterWords = [];
    for ($si = 0; $si < 20; $si++) {
        $scatterWords[] = $consonants[random_int(0, $consonantCount)]
                        . $vowels[random_int(0, $vowelCount)]
                        . $consonants[random_int(0, $consonantCount)];
    }
    $tagCount   = 0;
    $scatterIdx = 0;
    $scatterLen = count($scatterWords); // read-only in callback — no reference needed
    $html = preg_replace_callback(
        '/(<\/(?:p|div|td|th|li|h[1-6]|blockquote|article|section)>)/i',
        static function ($m) use ($scatterWords, $scatterLen, &$tagCount, &$scatterIdx) {
            // $tagCount and $scatterIdx are mutated on each match (pass by reference).
            // $scatterWords and $scatterLen are read-only (captured by value).
            $tagCount++;
            if ($tagCount % 4 === 0 && $scatterIdx < $scatterLen) {
                $word = $scatterWords[$scatterIdx++];
                return $m[1]
                    . '<span style="display:none" aria-hidden="true">' . $word . '</span>';
            }
            return $m[1];
        },
        $html
    );

    // Preheader: standard invisible span placed at the very start of <body>.
    // Email clients read this as the inbox preview snippet ("preheader text")
    // before the email is opened.  Invisible once the email is viewed.
    // Controlled independently by preheader_injection_enabled.
    $preheaderEnabled = ($campaign['preheader_injection_enabled'] ?? 1) ? true : false;
    if ($preheaderEnabled) {
        $preheaderSpan = '<span style="display:none !important;visibility:hidden;'
                       . 'mso-hide:all;font-size:1px;line-height:1px;'
                       . 'max-height:0px;max-width:0px;opacity:0;overflow:hidden;">'
                       . 'Ref: ' . $uniqueRef
                       . '</span>' . "\n";
        // Inject preheader span right after the opening <body> tag.
        // preg_replace_callback avoids backreference issues with special chars.
        if (preg_match('/<body[^>]*>/i', $html)) {
            $html = preg_replace_callback('/(<body[^>]*>)/i', static function ($m) use ($preheaderSpan) {
                return $m[1] . $preheaderSpan;
            }, $html, 1);
        } else {
            $html = $preheaderSpan . $html;
        }
    }

    // Footer (tailDiv): subtle visible element (10 px light-grey) at bottom of body.
    // Carries the unique ref + recipient + sub-second stamp.
    // Controlled independently by footer_injection_enabled.
    $footerEnabled = ($campaign['footer_injection_enabled'] ?? 1) ? true : false;
    if ($footerEnabled) {
        $tailDiv = '<div style="font-size:10px;color:#cccccc;text-align:center;line-height:1;padding:2px 0;">'
                 . 'Ref: ' . $uniqueRef . ' &#183; ' . $safeEmail . ' &#183; ' . $microsecondsStr
                 . '</div>' . "\n";
        if (stripos($html, '</body>') !== false) {
            return str_ireplace('</body>', $tailDiv . '</body>', $html);
        }
        if (stripos($html, '</html>') !== false) {
            return str_ireplace('</html>', $tailDiv . '</html>', $html);
        }
        return $html . "\n" . $tailDiv;
    }

    return $html;
}

/**
 * Content enrichment is intentionally disabled.
 *
 * Injecting subject text, preheader text, or filler sentences into the
 * campaign body would display meaningless boilerplate to recipients and
 * override the sender's intended message.  Razor2 uniqueness is handled
 * entirely by inject_email_token() (scatter markers + tailDiv).
 */
function enrich_email_content(string $html, array $campaign): string
{
    return $html;
}

/**
 * Get base URL for unsubscribe links
 */
function get_unsubscribe_url(int $campaignId, string $email): string
{
    $baseUrl = get_base_url();
    $token = base64_encode($email . ':' . $campaignId);
    return $baseUrl . '?t=unsubscribe&cid=' . $campaignId . '&e=' . urlencode($token);
}

/**
 * SMTP send (returns structured array)
 */
function smtp_send_mail(array $profile, array $campaign, string $to, string $html): array
{
    $log = [];

    $host = trim($profile['host'] ?? '');
    $port = (int)($profile['port'] ?? 587);
    $user = trim($profile['username'] ?? '');
    $pass = (string)($profile['password'] ?? '');

    $from = trim($campaign['from_email'] ?? '');

    if ($host === '' || $from === '' || $to === '') {
        return [
            'ok' => false,
            'type' => 'bounce',
            'code' => null,
            'msg' => 'SMTP: missing host/from/to',
            'stage' => 'connect',
            'log' => $log,
        ];
    }

    $remoteHost = ($port === 465 ? "ssl://{$host}" : $host);

    $errno = 0;
    $errstr = '';
    $socket = @fsockopen($remoteHost, $port, $errno, $errstr, 25);
    if (!$socket) {
        return [
            'ok' => false,
            'type' => 'bounce',
            'code' => null,
            'msg' => "SMTP connect error: {$errno} {$errstr}",
            'stage' => 'connect',
            'log' => $log,
        ];
    }
    stream_set_timeout($socket, 25);

    $read = function() use ($socket, &$log) {
        $data = '';
        while ($str = fgets($socket, 515)) {
            $data .= $str;
            $log[] = rtrim($str, "\r\n");
            if (strlen($str) < 4) break;
            if (substr($str, 3, 1) === ' ') break;
        }
        $code = isset($data[0]) ? substr($data, 0, 3) : null;
        $msg  = isset($data[4]) ? trim(substr($data, 4)) : trim($data);
        return [$code, $msg, $data];
    };
    $write = function(string $cmd) use ($socket, $read, &$log) {
        fputs($socket, $cmd . "\r\n");
        $log[] = 'C: ' . $cmd;
        return $read();
    };

    list($gcode, $gmsg) = $read();
    if (!is_string($gcode) || substr($gcode,0,1) !== '2') {
        fclose($socket);
        return [
            'ok' => false,
            'type' => 'bounce',
            'code' => $gcode,
            'msg' => $gmsg,
            'stage' => 'banner',
            'log' => $log,
        ];
    }

    list($ecode, $emsg) = $write('EHLO ' . smtp_get_ehlo_hostname($from));
    if (!is_string($ecode) || substr($ecode,0,1) !== '2') {
        list($hcode, $hmsg) = $write('HELO ' . smtp_get_ehlo_hostname($from));
        if (!is_string($hcode) || substr($hcode,0,1) !== '2') {
            fclose($socket);
            return [
                'ok' => false,
                'type' => 'bounce',
                'code' => $ecode ?: $hcode,
                'msg' => $emsg ?: $hmsg,
                'stage' => 'ehlo',
                'log' => $log,
            ];
        } else {
            $ecode = $hcode; $emsg = $hmsg;
        }
    }

    if ($port !== 465 && is_string($emsg) && stripos(implode("\n", $log), 'STARTTLS') !== false) {
        list($tcode, $tmsg) = $write('STARTTLS');
        if (!is_string($tcode) || substr($tcode, 0, 3) !== '220') {
            fclose($socket);
            return [
                'ok' => false,
                'type' => 'bounce',
                'code' => $tcode,
                'msg' => $tmsg,
                'stage' => 'starttls',
                'log' => $log,
            ];
        }
        $cryptoMethod = defined('STREAM_CRYPTO_METHOD_TLS_CLIENT')
            ? STREAM_CRYPTO_METHOD_TLS_CLIENT
            : (STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
               | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
               | STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT);

        if (!stream_socket_enable_crypto($socket, true, $cryptoMethod)) {
            fclose($socket);
            return [
                'ok' => false,
                'type' => 'bounce',
                'code' => null,
                'msg' => 'Unable to start TLS crypto',
                'stage' => 'starttls',
                'log' => $log,
            ];
        }
        list($ecode, $emsg) = $write('EHLO ' . smtp_get_ehlo_hostname($from));
    }

    if ($user !== '') {
        list($acode, $amsg) = $write('AUTH LOGIN');
        if (!is_string($acode) || substr($acode,0,1) !== '3') {
            fclose($socket);
            return ['ok'=>false, 'type'=>'bounce', 'code'=>$acode, 'msg'=>$amsg, 'stage'=>'auth', 'log'=>$log];
        }
        list($ucode, $umsg) = $write(base64_encode($user));
        if (!is_string($ucode) || substr($ucode,0,1) !== '3') {
            fclose($socket);
            return ['ok'=>false, 'type'=>'bounce', 'code'=>$ucode, 'msg'=>$umsg, 'stage'=>'auth_user', 'log'=>$log];
        }
        list($pcode, $pmsg) = $write(base64_encode($pass));
        if (!is_string($pcode) || substr($pcode,0,1) !== '2') {
            fclose($socket);
            return ['ok'=>false, 'type'=>'bounce', 'code'=>$pcode, 'msg'=>$pmsg, 'stage'=>'auth_pass', 'log'=>$log];
        }
    }

    list($mcode, $mmsg) = $write('MAIL FROM: <' . $from . '>');
    if (!is_string($mcode)) $mcode = null;
    if ($mcode !== null && $mcode !== '' && $mcode[0] === '5') {
        fclose($socket);
        return ['ok'=>false,'type'=>'bounce','code'=>$mcode,'msg'=>$mmsg,'stage'=>'mail_from','log'=>$log];
    } elseif ($mcode !== null && $mcode !== '' && $mcode[0] === '4') {
        fclose($socket);
        return ['ok'=>false,'type'=>'deferred','code'=>$mcode,'msg'=>$mmsg,'stage'=>'mail_from','log'=>$log];
    } elseif ($mcode === null) {
        fclose($socket);
        return ['ok'=>false,'type'=>'bounce','code'=>null,'msg'=>'MAIL FROM unknown response','stage'=>'mail_from','log'=>$log];
    }

    list($rcode, $rmsg) = $write('RCPT TO: <' . $to . '>');
    if (!is_string($rcode)) $rcode = null;
    if ($rcode !== null && $rcode[0] === '5') {
        fclose($socket);
        return ['ok'=>false,'type'=>'bounce','code'=>$rcode,'msg'=>$rmsg,'stage'=>'rcpt_to','log'=>$log];
    } elseif ($rcode !== null && $rcode[0] === '4') {
        fclose($socket);
        return ['ok'=>false,'type'=>'deferred','code'=>$rcode,'msg'=>$rmsg,'stage'=>'rcpt_to','log'=>$log];
    } elseif ($rcode === null) {
        fclose($socket);
        return ['ok'=>false,'type'=>'bounce','code'=>null,'msg'=>'RCPT TO unknown response','stage'=>'rcpt_to','log'=>$log];
    }

    list($dcode, $dmsg) = $write('DATA');
    if (!is_string($dcode) || substr($dcode,0,1) !== '3') {
        fclose($socket);
        return ['ok'=>false,'type'=>'bounce','code'=>$dcode,'msg'=>$dmsg,'stage'=>'data_cmd','log'=>$log];
    }

    $subject = encode_mime_header($campaign['subject'] ?? '');

    $headers = '';
    $headers .= "Date: " . gmdate('D, d M Y H:i:s T') . "\r\n";
    
    // Generate unique Message-ID (helps with spam scores)
    $headers .= "Message-ID: " . generate_message_id($from) . "\r\n";
    // X-Entity-Ref-ID: unique per email — ensures every email is header-unique even
    // if body content were somehow normalised to the same hash by a filter.
    $headers .= "X-Entity-Ref-ID: " . bin2hex(random_bytes(16)) . "\r\n";

    $fromDisplay = '';
    if (!empty($campaign['sender_name'])) {
        $fromDisplay = encode_mime_header($campaign['sender_name']) . " <{$from}>";
    } else {
        $fromDisplay = "<{$from}>";
    }
    $headers .= "From: {$fromDisplay}\r\n";
    // Explicit Sender: ensures envelope-from domain matches From: domain,
    // which reduces the HEADER_FROM_DIFFERENT_DOMAINS SpamAssassin penalty.
    $headers .= "Sender: <{$from}>\r\n";
    
    // Add Reply-To header (use campaign reply_to, then profile reply_to, fallback to From)
    $replyTo = trim($campaign['reply_to'] ?? '');
    if ($replyTo === '' && isset($profile['reply_to'])) {
        $replyTo = trim($profile['reply_to']);
    }
    if ($replyTo === '') {
        $replyTo = $from;
    }
    // Format reply-to with sender name if different from from address
    if ($replyTo !== $from && !empty($campaign['sender_name'])) {
        $replyToDisplay = encode_mime_header($campaign['sender_name']) . " <{$replyTo}>";
    } else {
        $replyToDisplay = "<{$replyTo}>";
    }
    $headers .= "Reply-To: {$replyToDisplay}\r\n";

    $headers .= "To: <{$to}>\r\n";
    if ($subject !== '') {
        $headers .= "Subject: {$subject}\r\n";
    }
    
    // Add List-Unsubscribe header (RFC 2369 - helps with spam scores)
    if (!empty($campaign['id']) && !empty($campaign['unsubscribe_enabled'])) {
        $unsubUrl = get_unsubscribe_url((int)$campaign['id'], $to);
        $headers .= "List-Unsubscribe: <{$unsubUrl}>\r\n";
        $headers .= "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n";
    }
    
    // Add bulk mail headers (improves spam scores)
    $headers .= "Precedence: bulk\r\n";
    $headers .= "X-Priority: 3\r\n";
    $headers .= "X-Mailer: MailSender/2.0\r\n";

    $headers .= "MIME-Version: 1.0\r\n";
    
    // Create multipart/alternative boundary
    $boundary = '----=_Part_' . md5(uniqid(rand(), true));
    $headers .= "Content-Type: multipart/alternative; boundary=\"{$boundary}\"\r\n\r\n";
    
    // Ensure HTML has proper tags
    $html = ensure_html_wrapper($html);
    
    // Enrich sparse emails with subject/preheader text (prevents HTML_IMAGE_ONLY_* rules)
    $html = enrich_email_content($html, $campaign);
    
    // Inject unique per-recipient token (preheader/footer controlled by campaign settings)
    $html = inject_email_token($html, $to, $campaign);
    
    // Generate plain text version from enriched/tokenized HTML
    $plainText = html_to_plain_text($html);
    // Explicitly prepend recipient address to the plain-text part.
    // This guarantees Razor2 hash uniqueness for the text/plain MIME part
    // independently of the HTML part, defeating engines that hash each part separately.
    // Strip control characters from $to before inserting into message body.
    $safeTo = preg_replace('/[\r\n\t\x00-\x1F\x7F]+/', '', $to);
    $plainText = "To: {$safeTo}\n\n" . $plainText;
    
    // Build multipart body
    $body = '';
    
    // Plain text part
    $body .= "--{$boundary}\r\n";
    $body .= "Content-Type: text/plain; charset=UTF-8\r\n";
    $body .= "Content-Transfer-Encoding: base64\r\n\r\n";
    $body .= chunk_split(base64_encode($plainText), 76, "\r\n");
    $body .= "\r\n";
    
    // HTML part
    $body .= "--{$boundary}\r\n";
    $body .= "Content-Type: text/html; charset=UTF-8\r\n";
    $body .= "Content-Transfer-Encoding: base64\r\n\r\n";
    $body .= chunk_split(base64_encode($html), 76, "\r\n");
    $body .= "\r\n";
    
    // End boundary
    $body .= "--{$boundary}--\r\n";

    $message = $headers . $body . "\r\n.\r\n";
    fputs($socket, $message);
    $log[] = 'C: <multipart message data...>';

    list($finalCode, $finalMsg) = $read();

    if (!is_string($finalCode) || $finalCode[0] !== '2') {
        fclose($socket);
        $type = 'bounce';
        if (is_string($finalCode) && $finalCode[0] === '4') $type = 'deferred';
        return ['ok'=>false,'type'=>$type,'code'=>$finalCode,'msg'=>$finalMsg,'stage'=>'data_end','log'=>$log];
    }

    $write('QUIT');
    fclose($socket);

    return [
        'ok'    => true,
        'type'  => 'delivered',
        'code'  => $finalCode,
        'msg'   => $finalMsg,
        'stage' => 'done',
        'log'   => $log,
    ];
}

/**
 * Send multiple emails using a single SMTP connection (batch sending)
 * This improves performance by reusing the connection instead of opening/closing for each email
 * 
 * @param array $profile SMTP profile configuration
 * @param array $campaign Campaign data
 * @param array $recipients Array of recipient email addresses
 * @param array $htmlMap Optional map of recipient => custom HTML (if not provided, same HTML for all)
 * @param callable|null $onProgress Optional callback(int $delta) called every $onProgressEvery emails.
 *                                   Use this to update progress_sent during the batch (not just after it).
 * @param int $onProgressEvery How often (number of emails) to invoke $onProgress (default: SMTP_PROGRESS_CHUNK_SIZE)
 * @return array Results with 'results' array containing per-recipient results
 */
function smtp_send_batch(array $profile, array $campaign, array $recipients, array $htmlMap = [], ?callable $onProgress = null, int $onProgressEvery = SMTP_PROGRESS_CHUNK_SIZE): array
{
    $log = [];
    $results = [];
    
    if (empty($recipients)) {
        return [
            'ok' => false,
            'error' => 'No recipients provided',
            'log' => $log,
            'results' => []
        ];
    }

    $host = trim($profile['host'] ?? '');
    $port = (int)($profile['port'] ?? 587);
    $user = trim($profile['username'] ?? '');
    $pass = (string)($profile['password'] ?? '');
    $from = trim($campaign['from_email'] ?? '');

    if ($host === '' || $from === '') {
        return [
            'ok' => false,
            'error' => 'Missing SMTP host or from address',
            'log' => $log,
            'results' => []
        ];
    }

    $remoteHost = ($port === 465 ? "ssl://{$host}" : $host);
    
    // Open connection
    $errno = 0;
    $errstr = '';
    $socket = @fsockopen($remoteHost, $port, $errno, $errstr, 25);
    if (!$socket) {
        $error = "SMTP connect error: {$errno} {$errstr}";
        $log[] = $error;
        return [
            'ok' => false,
            'error' => $error,
            'log' => $log,
            'results' => []
        ];
    }
    stream_set_timeout($socket, 25);

    $read = function() use ($socket, &$log) {
        $data = '';
        while ($str = fgets($socket, 515)) {
            $data .= $str;
            $log[] = rtrim($str, "\r\n");
            if (strlen($str) < 4) break;
            if (substr($str, 3, 1) === ' ') break;
        }
        $code = isset($data[0]) ? substr($data, 0, 3) : null;
        $msg  = isset($data[4]) ? trim(substr($data, 4)) : trim($data);
        return [$code, $msg, $data];
    };
    
    $write = function(string $cmd) use ($socket, $read, &$log) {
        fputs($socket, $cmd . "\r\n");
        $log[] = 'C: ' . $cmd;
        return $read();
    };

    // Banner
    list($gcode, $gmsg) = $read();
    if (!is_string($gcode) || substr($gcode,0,1) !== '2') {
        fclose($socket);
        return [
            'ok' => false,
            'error' => "SMTP banner failed: {$gcode} {$gmsg}",
            'log' => $log,
            'results' => []
        ];
    }

    // EHLO/HELO
    list($ecode, $emsg) = $write('EHLO ' . smtp_get_ehlo_hostname($from));
    if (!is_string($ecode) || substr($ecode,0,1) !== '2') {
        list($hcode, $hmsg) = $write('HELO ' . smtp_get_ehlo_hostname($from));
        if (!is_string($hcode) || substr($hcode,0,1) !== '2') {
            fclose($socket);
            return [
                'ok' => false,
                'error' => "EHLO/HELO failed: {$ecode} / {$hcode}",
                'log' => $log,
                'results' => []
            ];
        } else {
            $ecode = $hcode; $emsg = $hmsg;
        }
    }

    // STARTTLS if needed
    if ($port !== 465 && is_string($emsg) && stripos(implode("\n", $log), 'STARTTLS') !== false) {
        list($tcode, $tmsg) = $write('STARTTLS');
        if (!is_string($tcode) || substr($tcode, 0, 3) !== '220') {
            fclose($socket);
            return [
                'ok' => false,
                'error' => "STARTTLS failed: {$tcode} {$tmsg}",
                'log' => $log,
                'results' => []
            ];
        }
        $cryptoMethod = defined('STREAM_CRYPTO_METHOD_TLS_CLIENT')
            ? STREAM_CRYPTO_METHOD_TLS_CLIENT
            : (STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
               | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
               | STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT);

        if (!stream_socket_enable_crypto($socket, true, $cryptoMethod)) {
            fclose($socket);
            return [
                'ok' => false,
                'error' => 'Unable to start TLS crypto',
                'log' => $log,
                'results' => []
            ];
        }
        list($ecode, $emsg) = $write('EHLO ' . smtp_get_ehlo_hostname($from));
    }

    // AUTH
    if ($user !== '') {
        list($acode, $amsg) = $write('AUTH LOGIN');
        if (!is_string($acode) || substr($acode,0,1) !== '3') {
            fclose($socket);
            return [
                'ok' => false,
                'error' => "AUTH LOGIN failed: {$acode} {$amsg}",
                'log' => $log,
                'results' => []
            ];
        }
        list($ucode, $umsg) = $write(base64_encode($user));
        if (!is_string($ucode) || substr($ucode,0,1) !== '3') {
            fclose($socket);
            return [
                'ok' => false,
                'error' => "AUTH username rejected: {$ucode} {$umsg}",
                'log' => $log,
                'results' => []
            ];
        }
        list($pcode, $pmsg) = $write(base64_encode($pass));
        if (!is_string($pcode) || substr($pcode,0,1) !== '2') {
            fclose($socket);
            return [
                'ok' => false,
                'error' => "AUTH password rejected: {$pcode} {$pmsg}",
                'log' => $log,
                'results' => []
            ];
        }
    }

    // Now send each email using the same connection
    foreach ($recipients as $to) {
        $to = trim($to);
        if ($to === '') continue;
        
        // Determine HTML for this recipient
        $html = isset($htmlMap[$to]) ? $htmlMap[$to] : (isset($htmlMap['default']) ? $htmlMap['default'] : '');

        // MAIL FROM
        list($mcode, $mmsg) = $write('MAIL FROM: <' . $from . '>');
        if (!is_string($mcode)) $mcode = null;
        if ($mcode !== null && $mcode !== '' && $mcode[0] === '5') {
            $results[$to] = [
                'ok' => false,
                'type' => 'bounce',
                'code' => $mcode,
                'msg' => $mmsg,
                'stage' => 'mail_from',
                'log' => []
            ];
            // Try to continue with next recipient (some servers allow this)
            continue;
        } elseif ($mcode !== null && $mcode !== '' && $mcode[0] === '4') {
            $results[$to] = [
                'ok' => false,
                'type' => 'deferred',
                'code' => $mcode,
                'msg' => $mmsg,
                'stage' => 'mail_from',
                'log' => []
            ];
            continue;
        } elseif ($mcode === null) {
            $results[$to] = [
                'ok' => false,
                'type' => 'bounce',
                'code' => null,
                'msg' => 'MAIL FROM unknown response',
                'stage' => 'mail_from',
                'log' => []
            ];
            continue;
        }

        // RCPT TO
        list($rcode, $rmsg) = $write('RCPT TO: <' . $to . '>');
        if (!is_string($rcode)) $rcode = null;
        if ($rcode !== null && $rcode[0] === '5') {
            $results[$to] = [
                'ok' => false,
                'type' => 'bounce',
                'code' => $rcode,
                'msg' => $rmsg,
                'stage' => 'rcpt_to',
                'log' => []
            ];
            continue;
        } elseif ($rcode !== null && $rcode[0] === '4') {
            $results[$to] = [
                'ok' => false,
                'type' => 'deferred',
                'code' => $rcode,
                'msg' => $rmsg,
                'stage' => 'rcpt_to',
                'log' => []
            ];
            continue;
        } elseif ($rcode === null) {
            $results[$to] = [
                'ok' => false,
                'type' => 'bounce',
                'code' => null,
                'msg' => 'RCPT TO unknown response',
                'stage' => 'rcpt_to',
                'log' => []
            ];
            continue;
        }

        // DATA command
        list($dcode, $dmsg) = $write('DATA');
        if (!is_string($dcode) || substr($dcode,0,1) !== '3') {
            $results[$to] = [
                'ok' => false,
                'type' => 'bounce',
                'code' => $dcode,
                'msg' => $dmsg,
                'stage' => 'data_cmd',
                'log' => []
            ];
            continue;
        }

        // Build message
        $subject = encode_mime_header($campaign['subject'] ?? '');
        $headers = '';
        $headers .= "Date: " . gmdate('D, d M Y H:i:s T') . "\r\n";
        
        // Generate unique Message-ID
        $headers .= "Message-ID: " . generate_message_id($from) . "\r\n";
        // X-Entity-Ref-ID: unique per email
        $headers .= "X-Entity-Ref-ID: " . bin2hex(random_bytes(16)) . "\r\n";

        $fromDisplay = '';
        if (!empty($campaign['sender_name'])) {
            $fromDisplay = encode_mime_header($campaign['sender_name']) . " <{$from}>";
        } else {
            $fromDisplay = "<{$from}>";
        }
        $headers .= "From: {$fromDisplay}\r\n";
        // Explicit Sender: to reduce HEADER_FROM_DIFFERENT_DOMAINS penalty
        $headers .= "Sender: <{$from}>\r\n";
        
        // Add Reply-To header (use campaign reply_to, then profile reply_to, fallback to From)
        $replyTo = trim($campaign['reply_to'] ?? '');
        if ($replyTo === '' && isset($profile['reply_to'])) {
            $replyTo = trim($profile['reply_to']);
        }
        if ($replyTo === '') {
            $replyTo = $from;
        }
        // Format reply-to with sender name if different from from address
        if ($replyTo !== $from && !empty($campaign['sender_name'])) {
            $replyToDisplay = encode_mime_header($campaign['sender_name']) . " <{$replyTo}>";
        } else {
            $replyToDisplay = "<{$replyTo}>";
        }
        $headers .= "Reply-To: {$replyToDisplay}\r\n";
        
        $headers .= "To: <{$to}>\r\n";
        if ($subject !== '') {
            $headers .= "Subject: {$subject}\r\n";
        }
        
        // Add List-Unsubscribe header
        if (!empty($campaign['id']) && !empty($campaign['unsubscribe_enabled'])) {
            $unsubUrl = get_unsubscribe_url((int)$campaign['id'], $to);
            $headers .= "List-Unsubscribe: <{$unsubUrl}>\r\n";
            $headers .= "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n";
        }
        
        // Add bulk mail headers
        $headers .= "Precedence: bulk\r\n";
        $headers .= "X-Priority: 3\r\n";
        $headers .= "X-Mailer: MailSender/2.0\r\n";
        
        $headers .= "MIME-Version: 1.0\r\n";
        
        // Create multipart/alternative boundary
        $boundary = '----=_Part_' . md5(uniqid(rand(), true));
        $headers .= "Content-Type: multipart/alternative; boundary=\"{$boundary}\"\r\n\r\n";
        
        // Ensure HTML has proper tags
        $html = ensure_html_wrapper($html);
        
        // Enrich sparse emails with subject/preheader text (prevents HTML_IMAGE_ONLY_* rules)
        $html = enrich_email_content($html, $campaign);
        
        // Inject unique per-recipient token (preheader/footer controlled by campaign settings)
        $html = inject_email_token($html, $to, $campaign);
        
        // Generate plain text version from enriched/tokenized HTML
        $plainText = html_to_plain_text($html);
        // Explicitly prepend recipient address to the plain-text part.
        // Strip control characters from $to before inserting into message body.
        $safeTo = preg_replace('/[\r\n\t\x00-\x1F\x7F]+/', '', $to);
        $plainText = "To: {$safeTo}\n\n" . $plainText;
        
        // Build multipart body
        $body = '';
        
        // Plain text part
        $body .= "--{$boundary}\r\n";
        $body .= "Content-Type: text/plain; charset=UTF-8\r\n";
        $body .= "Content-Transfer-Encoding: base64\r\n\r\n";
        $body .= chunk_split(base64_encode($plainText), 76, "\r\n");
        $body .= "\r\n";
        
        // HTML part
        $body .= "--{$boundary}\r\n";
        $body .= "Content-Type: text/html; charset=UTF-8\r\n";
        $body .= "Content-Transfer-Encoding: base64\r\n\r\n";
        $body .= chunk_split(base64_encode($html), 76, "\r\n");
        $body .= "\r\n";
        
        // End boundary
        $body .= "--{$boundary}--\r\n";
        
        // Send message data
        fputs($socket, $headers . $body);
        // Send termination sequence (CRLF.CRLF)
        fputs($socket, ".\r\n");
        $log[] = 'C: <multipart message data...>';

        list($finalCode, $finalMsg) = $read();

        if (!is_string($finalCode) || $finalCode[0] !== '2') {
            $type = 'bounce';
            if (is_string($finalCode) && $finalCode[0] === '4') $type = 'deferred';
            $results[$to] = [
                'ok' => false,
                'type' => $type,
                'code' => $finalCode,
                'msg' => $finalMsg,
                'stage' => 'data_end',
                'log' => []
            ];
        } else {
            $results[$to] = [
                'ok' => true,
                'type' => 'delivered',
                'code' => $finalCode,
                'msg' => $finalMsg,
                'stage' => 'done',
                'log' => []
            ];
        }
        
        // Fire inline progress callback every $onProgressEvery emails.
        // This lets callers update progress_sent DURING the batch (not just after it returns),
        // so the UI sees incremental progress from all parallel workers simultaneously.
        if ($onProgress !== null) {
            $resultsSoFar = count($results);
            if (($resultsSoFar % $onProgressEvery) === 0) {
                call_user_func($onProgress, $onProgressEvery);
            }
        }
    }

    // Close connection
    $write('QUIT');
    fclose($socket);

    // Check overall success
    $successCount = 0;
    foreach ($results as $result) {
        if (!empty($result['ok'])) $successCount++;
    }

    return [
        'ok' => $successCount > 0,
        'total' => count($recipients),
        'success' => $successCount,
        'failed' => count($recipients) - $successCount,
        'log' => $log,
        'results' => $results
    ];
}

function api_send_mail(array $profile, array $campaign, string $to, string $html): array
{
    $apiUrl = trim($profile['api_url'] ?? '');
    $apiKey = trim($profile['api_key'] ?? '');
    $from   = trim($campaign['from_email'] ?? '');
    $provider = trim($profile['provider'] ?? '');

    $log = [];

    if ($apiUrl === '' || $from === '' || $to === '') {
        return [
            'ok' => false,
            'type' => 'bounce',
            'code' => null,
            'msg' => 'API: missing api_url/from/to',
            'stage' => 'api',
            'log' => $log,
        ];
    }

    $headers = [
        'Content-Type: application/json',
    ];
    if ($apiKey !== '') {
        $headers[] = 'Authorization: Bearer ' . $apiKey;
    }

    if (!empty($profile['headers_json'])) {
        $extra = json_decode($profile['headers_json'], true);
        if (is_array($extra)) {
            foreach ($extra as $k => $v) {
                $headers[] = $k . ': ' . $v;
            }
        }
    }

    // Build provider-specific payload
    $payload = api_build_payload($provider, $from, $to, $campaign, $html);
    
    if ($payload === null) {
        return [
            'ok' => false,
            'type' => 'bounce',
            'code' => null,
            'msg' => 'API: unsupported provider format',
            'stage' => 'api',
            'log' => $log,
        ];
    }

    $log[] = 'API POST ' . $apiUrl . ' Provider: ' . $provider;
    $log[] = 'Payload: ' . substr(json_encode($payload), 0, 500);

    $ch = curl_init($apiUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
    curl_setopt($ch, CURLOPT_TIMEOUT, 25);
    apply_proxy_to_curl($ch, $profile);

    $resp = curl_exec($ch);
    if ($resp === false) {
        $err = curl_error($ch);
        curl_close($ch);
        $log[] = 'CURL ERROR: ' . $err;
        return [
            'ok' => false,
            'type' => 'bounce',
            'code' => null,
            'msg' => 'API cURL error: ' . $err,
            'stage' => 'api',
            'log' => $log,
        ];
    }

    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    $log[] = 'API RESPONSE CODE: ' . $code . ' BODY: ' . substr($resp,0,1000);

    if ($code >= 200 && $code < 300) {
        return [
            'ok' => true,
            'type' => 'delivered',
            'code' => (string)$code,
            'msg'  => substr($resp,0,1000),
            'stage'=> 'api',
            'log'  => $log,
        ];
    }

    $etype = ($code >= 500) ? 'bounce' : 'deferred';
    return [
        'ok' => false,
        'type' => $etype,
        'code' => (string)$code,
        'msg'  => substr($resp,0,1000),
        'stage'=> 'api',
        'log'  => $log,
    ];
}

/**
 * Build API payload based on provider
 */
function api_build_payload(string $provider, string $from, string $to, array $campaign, string $html): ?array
{
    $subject = $campaign['subject'] ?? '';
    $fromName = $campaign['sender_name'] ?? '';
    
    // Ensure HTML has proper tags
    $html = ensure_html_wrapper($html);
    
    // Enrich sparse emails with subject/preheader text (prevents HTML_IMAGE_ONLY_* rules)
    $html = enrich_email_content($html, $campaign);
    
    // Inject unique per-recipient token (preheader/footer controlled by campaign settings)
    $html = inject_email_token($html, $to, $campaign);
    
    // Generate plain text version from enriched/tokenized HTML
    $plainText = html_to_plain_text($html);
    // Prepend recipient address to the plain-text part for independent Razor2 uniqueness.
    // Validate and strip control characters to prevent header injection.
    $safeTo = filter_var($to, FILTER_VALIDATE_EMAIL) !== false
        ? preg_replace('/[\r\n\t\x00-\x1F\x7F]+/', '', $to)
        : '';
    if ($safeTo !== '') {
        $plainText = "To: {$safeTo}\n\n" . $plainText;
    }
    
    // Detect provider from URL if not explicitly set
    $providerLower = strtolower($provider);
    
    // SparkPost format
    if ($providerLower === 'sparkpost' || strpos($providerLower, 'sparkpost') !== false) {
        return [
            'options' => [
                'open_tracking' => false,
                'click_tracking' => false,
            ],
            'content' => [
                'from' => [
                    'email' => $from,
                    'name' => $fromName ?: $from,
                ],
                'subject' => $subject,
                'html' => $html,
                'text' => $plainText,
            ],
            'recipients' => [
                ['address' => $to]
            ]
        ];
    }
    
    // SendGrid format
    if ($providerLower === 'sendgrid api' || strpos($providerLower, 'sendgrid') !== false) {
        $payload = [
            'personalizations' => [
                [
                    'to' => [
                        ['email' => $to]
                    ]
                ]
            ],
            'from' => [
                'email' => $from
            ],
            'subject' => $subject,
            'content' => [
                [
                    'type' => 'text/plain',
                    'value' => $plainText
                ],
                [
                    'type' => 'text/html',
                    'value' => $html
                ]
            ],
            // Disable SendGrid's own tracking to prevent sendgrid.net URLs being
            // injected into delivered emails (which trigger URIBL_GREY).
            // We handle our own open/click tracking via build_tracked_html().
            'tracking_settings' => [
                'click_tracking'        => ['enable' => false, 'enable_text' => false],
                'open_tracking'         => ['enable' => false],
                'subscription_tracking' => ['enable' => false],
            ],
        ];
        
        if ($fromName) {
            $payload['from']['name'] = $fromName;
        }
        
        return $payload;
    }
    
    // Mailgun format
    if ($providerLower === 'mailgun' || strpos($providerLower, 'mailgun') !== false) {
        // Note: Mailgun typically uses form-data, not JSON
        // This is a JSON approximation - actual implementation may need form-data
        return [
            'from'        => $fromName ? "{$fromName} <{$from}>" : $from,
            'to'          => $to,
            'subject'     => $subject,
            'text'        => $plainText,
            'html'        => $html,
            // Disable Mailgun's own tracking to prevent mailgun-tracked URLs
            // appearing in delivered emails (triggers URIBL rules).
            'o:tracking'  => 'no',
            'o:tracking-clicks' => 'no',
            'o:tracking-opens'  => 'no',
        ];
    }
    
    // MailJet format
    if ($providerLower === 'mailjet api' || strpos($providerLower, 'mailjet') !== false) {
        return [
            'Messages' => [
                [
                    'From' => [
                        'Email' => $from,
                        'Name' => $fromName ?: $from,
                    ],
                    'To' => [
                        [
                            'Email' => $to
                        ]
                    ],
                    'Subject' => $subject,
                    'TextPart' => $plainText,
                    'HTMLPart' => $html,
                ]
            ]
        ];
    }
    
    // Generic/fallback format (similar to SendGrid)
    return [
        'personalizations' => [
            [
                'to' => [
                    ['email' => $to]
                ]
            ]
        ],
        'from' => [
            'email' => $from,
            'name' => $fromName ?: $from
        ],
        'subject' => $subject,
        'content' => [
            [
                'type' => 'text/plain',
                'value' => $plainText
            ],
            [
                'type' => 'text/html',
                'value' => $html
            ]
        ]
    ];
}

/**
 * Build a multi-recipient API payload for a single batch request.
 *
 * Supports SendGrid-style (personalizations[].to[]), SparkPost (recipients[]),
 * MailJet (Messages[]), and a generic SendGrid-compatible fallback.
 *
 * @param string $provider   Profile provider name
 * @param string $from       Sender email address
 * @param string $fromName   Sender display name
 * @param array  $toEmails   Recipient email addresses for this batch
 * @param string $subject    Email subject line
 * @param string $html       Shared HTML body for all recipients in this batch
 * @param string $plainText  Plain-text version of the body
 * @return array             Provider-formatted payload
 */
function api_build_multi_recipient_payload(
    string $provider,
    string $from,
    string $fromName,
    array  $toEmails,
    string $subject,
    string $html,
    string $plainText
): array {
    $providerLower = strtolower($provider);

    // SparkPost – recipients[] array
    if ($providerLower === 'sparkpost' || strpos($providerLower, 'sparkpost') !== false) {
        return [
            'options' => ['open_tracking' => false, 'click_tracking' => false],
            'content' => [
                'from'    => ['email' => $from, 'name' => $fromName ?: $from],
                'subject' => $subject,
                'html'    => $html,
                'text'    => $plainText,
            ],
            'recipients' => array_map(static fn($e) => ['address' => $e], $toEmails),
        ];
    }

    // MailJet – Messages[] array (one entry per recipient)
    if ($providerLower === 'mailjet api' || strpos($providerLower, 'mailjet') !== false) {
        $messages = [];
        foreach ($toEmails as $email) {
            $messages[] = [
                'From'     => ['Email' => $from, 'Name' => $fromName ?: $from],
                'To'       => [['Email' => $email]],
                'Subject'  => $subject,
                'TextPart' => $plainText,
                'HTMLPart' => $html,
            ];
        }
        return ['Messages' => $messages];
    }

    // SendGrid / generic fallback – personalizations[].to[]
    $personalizations = [];
    foreach ($toEmails as $email) {
        $personalizations[] = ['to' => [['email' => $email]]];
    }
    $payload = [
        'personalizations' => $personalizations,
        'from'    => $fromName ? ['email' => $from, 'name' => $fromName] : ['email' => $from],
        'subject' => $subject,
        'content' => [
            ['type' => 'text/plain', 'value' => $plainText],
            ['type' => 'text/html',  'value' => $html],
        ],
    ];
    // Disable SendGrid's own tracking (same as single-send path)
    if ($providerLower === 'sendgrid api' || strpos($providerLower, 'sendgrid') !== false) {
        $payload['tracking_settings'] = [
            'click_tracking'        => ['enable' => false, 'enable_text' => false],
            'open_tracking'         => ['enable' => false],
            'subscription_tracking' => ['enable' => false],
        ];
    }
    return $payload;
}

/**
 * Send emails via API using true batch delivery with curl_multi.
 *
 * Architecture (phases per requirements):
 *   Phase 2 – Build all batches (API_RECIPIENTS_PER_REQUEST each) in memory
 *   Phase 3 – JSON-encode all payloads before entering the send loop
 *   Phase 4 – Deliver via a fixed curl_multi pool; no file I/O or DB queries here
 *
 * Each cURL handle in the pool:
 *   - Is initialised once and reused across batches (CURLOPT_FORBID_REUSE = false)
 *   - Uses HTTP/2 (CURLOPT_HTTP_VERSION = CURL_HTTP_VERSION_2_0)
 *   - Applies the same proxy for the lifetime of the pool
 *
 * @param array $profile         API profile configuration
 * @param array $campaign        Campaign data
 * @param array $recipients      Recipient email addresses
 * @param array $htmlMap         Optional per-recipient HTML (first recipient's HTML used as batch template)
 * @param callable|null $onProgress    Fired with (int $count) after each batch — for progress_sent updates
 * @param callable|null $onBatchComplete Fired with (array $emails, array $results) after each batch, BEFORE
 *                                       $onProgress — lets callers insert event rows in real time so that
 *                                       'delivered' count tracks with progress_sent during the send
 * @return array            ['ok' => bool, 'results' => [email => result], 'batches_sent' => int]
 */
function api_send_batch(array $profile, array $campaign, array $recipients, array $htmlMap = [], ?callable $onProgress = null, ?callable $onBatchComplete = null): array
{
    $results = [];

    if (empty($recipients)) {
        return ['ok' => false, 'error' => 'No recipients provided', 'results' => []];
    }

    $apiUrl   = trim($profile['api_url'] ?? '');
    $apiKey   = trim($profile['api_key'] ?? '');
    $from     = trim($campaign['from_email'] ?? '');
    $fromName = trim($profile['sender_name'] ?? $campaign['sender_name'] ?? '');
    $provider = trim($profile['provider'] ?? '');
    $subject  = $campaign['subject'] ?? '';

    if ($apiUrl === '' || $from === '') {
        return ['ok' => false, 'error' => 'Missing API URL or from address', 'results' => []];
    }

    // ── HTTP headers built once and shared across all handles ───────────────────
    $headers = ['Content-Type: application/json'];
    if ($apiKey !== '') {
        $headers[] = 'Authorization: Bearer ' . $apiKey;
    }
    if (!empty($profile['headers_json'])) {
        $extra = json_decode($profile['headers_json'], true);
        if (is_array($extra)) {
            foreach ($extra as $k => $v) {
                $headers[] = $k . ': ' . $v;
            }
        }
    }

    // ── Phase 2 – Build all batch payloads in memory ─────────────────────────────
    $batchSize = max(1, API_RECIPIENTS_PER_REQUEST);
    $batches   = array_chunk($recipients, $batchSize);
    $payloads  = []; // [['batch' => [...emails], 'json' => '...']]

    foreach ($batches as $batch) {
        // Use first recipient's HTML as shared template for the batch.
        // NOTE: In batch mode all recipients in the same batch share the same HTML content.
        // Per-recipient tokens (inject_email_token) are applied using the first recipient's
        // address so that at least a unique ref is embedded per batch.
        $firstEmail = reset($batch);
        $sharedHtml = $htmlMap[$firstEmail] ?? $htmlMap['default'] ?? '';

        // If no pre-built HTML was provided (e.g. caller passed [] to save memory for large
        // lists), build it on the fly for this batch's first recipient.  This means only
        // one HTML page is built per batch (not per recipient), which is correct for API
        // sends where all recipients in a batch share the same email body.
        if ($sharedHtml === '') {
            $sharedHtml = build_tracked_html($campaign, $firstEmail);
        }

        // Apply content transformations once per batch (not per recipient)
        $sharedHtml      = ensure_html_wrapper($sharedHtml);
        $sharedHtml      = enrich_email_content($sharedHtml, $campaign);
        $sharedHtml      = inject_email_token($sharedHtml, $firstEmail, $campaign);
        $sharedPlainText = html_to_plain_text($sharedHtml);

        $payload = api_build_multi_recipient_payload(
            $provider, $from, $fromName, $batch, $subject, $sharedHtml, $sharedPlainText
        );

        // ── Phase 3 – JSON-encode before entering the send loop ──────────────────
        $payloads[] = ['batch' => $batch, 'json' => json_encode($payload)];
    }

    if (empty($payloads)) {
        return ['ok' => false, 'error' => 'No sendable batches', 'results' => $results];
    }

    // ── Phase 4 – Deliver via fixed curl_multi pool ───────────────────────────────
    // Pool handles are initialised once; proxy assigned once per handle (stable per pool).
    $poolSize   = min(API_MULTI_POOL_SIZE, count($payloads));
    $mh         = curl_multi_init();
    $handlePool = []; // slot => ['ch' => resource, 'batch_idx' => int|null]

    for ($i = 0; $i < $poolSize; $i++) {
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL            => $apiUrl,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_TIMEOUT        => 120,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS      => 3,
            // HTTP/2 enables multiplexing and reduces proxy overhead
            CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_2_0,
            // Keep connections alive across requests (never tear down between batches)
            CURLOPT_FORBID_REUSE   => false,
            CURLOPT_FRESH_CONNECT  => false,
        ]);
        // Proxy assigned once per handle – stable for the lifetime of the pool
        apply_proxy_to_curl($ch, $profile);
        $handlePool[$i] = ['ch' => $ch, 'batch_idx' => null];
    }

    $payloadIdx    = 0;
    $totalPayloads = count($payloads);
    $batchesSent   = 0;

    // Seed the pool with the first wave of payloads
    foreach ($handlePool as $slot => &$entry) {
        if ($payloadIdx >= $totalPayloads) break;
        curl_setopt($entry['ch'], CURLOPT_POSTFIELDS, $payloads[$payloadIdx]['json']);
        curl_multi_add_handle($mh, $entry['ch']);
        $entry['batch_idx'] = $payloadIdx;
        $payloadIdx++;
    }
    unset($entry);

    // Drive the curl_multi event loop
    do {
        // Keep calling curl_multi_exec until it stops needing immediate re-invocation.
        // Using a while loop here (instead of `if ... continue`) ensures all internal
        // libcurl state is fully flushed before we read the info queue — otherwise
        // CURLM_CALL_MULTI_PERFORM could cause the outer loop to exit (when $running=0)
        // before the completed handles are harvested, silently dropping those batches.
        while (($status = curl_multi_exec($mh, $running)) === CURLM_CALL_MULTI_PERFORM);

        // Harvest completed handles and re-feed them
        while (($info = curl_multi_info_read($mh)) !== false) {
            if ($info['msg'] !== CURLMSG_DONE) continue;

            $doneCh = $info['handle'];

            // Identify which pool slot owns this handle
            $doneSlot = null;
            foreach ($handlePool as $slot => $entry) {
                if ($entry['ch'] === $doneCh) { $doneSlot = $slot; break; }
            }
            // Guard: if handle is not found in the pool, skip (should not happen)
            if ($doneSlot === null) continue;

            $doneBatchIdx = $handlePool[$doneSlot]['batch_idx'];
            $doneBatch    = $payloads[$doneBatchIdx]['batch'];

            // Remove from multi before reuse
            curl_multi_remove_handle($mh, $doneCh);

            if ($info['result'] === CURLE_OK) {
                $resp = curl_multi_getcontent($doneCh);
                $code = curl_getinfo($doneCh, CURLINFO_HTTP_CODE);
                $ok   = ($code >= 200 && $code < 300);
                // Non-2xx API responses are 'deferred': an HTTP error from the email API
                // (4xx = bad request/rate limit, 5xx = server error) is NOT evidence that
                // the recipient addresses are invalid. Mark deferred so recipients are not
                // permanently unsubscribed.
                foreach ($doneBatch as $email) {
                    $results[$email] = [
                        'ok'    => $ok,
                        'type'  => $ok ? 'delivered' : 'deferred',
                        'code'  => (string)$code,
                        'msg'   => substr($resp ?? '', 0, 1000),
                        'stage' => 'api',
                        'log'   => [],
                    ];
                }
            } else {
                // cURL/network error (proxy failure, timeout, connection refused) is NOT
                // a permanent bounce. Mark deferred so recipients can be retried.
                $errMsg = curl_error($doneCh);
                foreach ($doneBatch as $email) {
                    $results[$email] = [
                        'ok'    => false,
                        'type'  => 'deferred',
                        'code'  => null,
                        'msg'   => 'cURL error: ' . $errMsg,
                        'stage' => 'api',
                        'log'   => [],
                    ];
                }
            }
            $batchesSent++;

            // Fire event-insertion callback FIRST so DB event rows exist before progress is updated.
            // This keeps 'delivered' count in sync with progress_sent throughout the send.
            if ($onBatchComplete !== null) {
                $batchResults = [];
                foreach ($doneBatch as $email) {
                    $batchResults[$email] = $results[$email];
                }
                $onBatchComplete($doneBatch, $batchResults);
            }

            // Report per-batch progress so callers can update the DB in real time
            if ($onProgress !== null) {
                $onProgress(count($doneBatch));
            }

            // Re-feed this handle with the next payload (connection reuse).
            // A brief pause before re-submission paces the request rate and prevents
            // the API server from responding with rate-limit (429) errors.
            if ($payloadIdx < $totalPayloads) {
                if (API_BATCH_INTER_REQUEST_DELAY_US > 0) {
                    usleep(API_BATCH_INTER_REQUEST_DELAY_US);
                }
                curl_setopt($doneCh, CURLOPT_POSTFIELDS, $payloads[$payloadIdx]['json']);
                curl_multi_add_handle($mh, $doneCh);
                $handlePool[$doneSlot]['batch_idx'] = $payloadIdx;
                $payloadIdx++;
                // NOTE: do not manually increment $running — curl_multi_exec() manages it
            }
        }

        if ($running > 0) {
            curl_multi_select($mh, 0.01);
        }
        // Loop until every batch has been harvested, NOT just until libcurl's $running hits 0.
        // $running is set by curl_multi_exec BEFORE handles are re-added in the harvest loop
        // above, so it can be 0 (stale) even while newly re-fed handles are waiting to be
        // executed.  Using $batchesSent < $totalPayloads guarantees we never exit the loop
        // before all payloads have been processed.
    } while ($batchesSent < $totalPayloads);

    // Close all pool handles (connections are torn down here, not per-batch)
    foreach ($handlePool as $entry) {
        curl_close($entry['ch']);
    }
    curl_multi_close($mh);

    return ['ok' => true, 'results' => $results, 'batches_sent' => $batchesSent];
}

function get_campaign(PDO $pdo, int $id) {
    $stmt = $pdo->prepare("SELECT * FROM campaigns WHERE id = ?");
    $stmt->execute([$id]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!$row) return false;
    if (isset($row['html']) && is_string($row['html'])) {
        if (strpos($row['html'], 'BASE64:') === 0) {
            $decoded = base64_decode(substr($row['html'], 7));
            if ($decoded !== false) {
                $row['html'] = $decoded;
            }
        }
    }
    return $row;
}

function get_campaign_stats(PDO $pdo, int $id) {
    $stats = [
        'delivered'   => 0,
        'delivered_raw' => 0,
        'open'        => 0,
        'click'       => 0,
        'bounce'      => 0,
        'spam'        => 0,
        'unsubscribe' => 0,
        'sent_attempts' => 0,
        'target' => 0,
        'target_after_bounces' => 0,
    ];

    // OPTIMIZED: Use indexed query for better performance
    $stmt = $pdo->prepare("SELECT event_type, COUNT(*) as cnt FROM events USE INDEX (idx_campaign_event) WHERE campaign_id = ? GROUP BY event_type");
    $stmt->execute([$id]);
    foreach ($stmt as $row) {
        $etype = $row['event_type'];
        $cnt = (int)$row['cnt'];
        if ($etype === 'delivered') {
            $stats['delivered_raw'] = $cnt;
        } elseif ($etype === 'bounce') {
            $stats['bounce'] = $cnt;
        } elseif (isset($stats[$etype])) {
            $stats[$etype] = $cnt;
        }
    }

    $stats['delivered'] = max(0, $stats['delivered_raw'] - $stats['bounce']);
    $stats['sent_attempts'] = $stats['delivered_raw'] + $stats['bounce'];

    try {
        // OPTIMIZED: Use indexed query for skipped_unsubscribe count
        $stmt2 = $pdo->prepare("SELECT COUNT(*) FROM events USE INDEX (idx_campaign_event) WHERE campaign_id = ? AND event_type = ?");
        $stmt2->execute([$id, 'skipped_unsubscribe']);
        $skipped = (int)$stmt2->fetchColumn();
        if ($skipped > 0) {
            $stats['unsubscribe'] += $skipped;
        }
    } catch (Exception $e) {}

    try {
        $stmtc = $pdo->prepare("SELECT total_recipients FROM campaigns WHERE id = ? LIMIT 1");
        $stmtc->execute([$id]);
        $camp = $stmtc->fetch(PDO::FETCH_ASSOC);
        $target = 0;
        if ($camp) {
            // Audience column doesn't exist in user's table, use total_recipients instead
            $target = (int)($camp['total_recipients'] ?? 0);
        }
        $stats['target'] = $target;
        $stats['target_after_bounces'] = max(0, $target - $stats['bounce']);
    } catch (Exception $e) {}

    return $stats;
}

/**
 * Update campaign progress for real-time stats
 */
function update_campaign_progress(PDO $pdo, int $campaignId, int $sent, int $total, string $status = 'sending') {
    try {
        // When updating to 'completed' status or 'sending' status, use GREATEST to ensure we don't overwrite
        // a higher value that workers may have incremented to
        if ($status === 'completed' || $status === 'sending') {
            $stmt = $pdo->prepare("UPDATE campaigns SET progress_sent = GREATEST(progress_sent, ?), progress_total = ?, progress_status = ? WHERE id = ?");
            $stmt->execute([$sent, $total, $status, $campaignId]);
        } else {
            // For other statuses (queued, draft), just set the values directly
            $stmt = $pdo->prepare("UPDATE campaigns SET progress_sent = ?, progress_total = ?, progress_status = ? WHERE id = ?");
            $stmt->execute([$sent, $total, $status, $campaignId]);
        }
    } catch (Exception $e) {
        // Ignore errors gracefully (column might not exist in older schemas)
        error_log("Failed to update campaign progress: " . $e->getMessage());
    }
}

/**
 * Get campaign progress for real-time stats
 */
function get_campaign_progress(PDO $pdo, int $campaignId): array {
    try {
        $stmt = $pdo->prepare("SELECT progress_sent, progress_total, progress_status, status FROM campaigns WHERE id = ?");
        $stmt->execute([$campaignId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if ($row) {
            return [
                'sent' => (int)$row['progress_sent'],
                'total' => (int)$row['progress_total'],
                'status' => $row['progress_status'] ?? 'draft',
                'campaign_status' => $row['status'] ?? 'draft',
                'percentage' => $row['progress_total'] > 0 ? round(($row['progress_sent'] / $row['progress_total']) * 100, 1) : 0
            ];
        }
    } catch (Exception $e) {
        // Graceful fallback for older schemas without progress columns
        error_log("Failed to get campaign progress: " . $e->getMessage());
    }
    return ['sent' => 0, 'total' => 0, 'status' => 'draft', 'campaign_status' => 'draft', 'percentage' => 0];
}

function get_profiles(PDO $pdo) {
    $stmt = $pdo->query("SELECT * FROM sending_profiles ORDER BY id ASC");
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

function get_rotation_settings(PDO $pdo) {
    $stmt = $pdo->query("SELECT * FROM rotation_settings WHERE id = 1");
    $row  = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!$row) {
        $pdo->exec("INSERT INTO rotation_settings(id) VALUES(1)");
        $stmt = $pdo->query("SELECT * FROM rotation_settings WHERE id = 1");
        $row  = $stmt->fetch(PDO::FETCH_ASSOC);
    }
    return $row;
}

function update_rotation_settings(PDO $pdo, array $data) {
    // Only update columns that exist in the schema (rotation_enabled, workers, messages_per_worker)
    // Older columns like batch_size and max_sends_per_profile may not exist
    $stmt = $pdo->prepare("
        UPDATE rotation_settings
        SET rotation_enabled = :rotation_enabled,
            workers = :workers,
            messages_per_worker = :messages_per_worker
        WHERE id = 1
    ");
    $stmt->execute([
        ':rotation_enabled'      => $data['rotation_enabled'],
        ':workers'               => $data['workers'] ?? 4,
        ':messages_per_worker'   => $data['messages_per_worker'] ?? 100,
    ]);
}

function get_contact_lists(PDO $pdo) {
    $sql = "
        SELECT l.*, COUNT(c.id) AS contact_count
        FROM contact_lists l
        LEFT JOIN contacts c ON c.list_id = l.id
        GROUP BY l.id
        ORDER BY l.created_at DESC
    ";
    $stmt = $pdo->query($sql);
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

function get_contact_list(PDO $pdo, int $id) {
    $stmt = $pdo->prepare("SELECT * FROM contact_lists WHERE id = ?");
    $stmt->execute([$id]);
    return $stmt->fetch(PDO::FETCH_ASSOC);
}

function get_contacts_for_list(PDO $pdo, int $listId) {
    $stmt = $pdo->prepare("SELECT * FROM contacts WHERE list_id = ? ORDER BY created_at DESC");
    $stmt->execute([$listId]);
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

function pick_next_profile(PDO $pdo) {
    $settings = get_rotation_settings($pdo);
    if ((int)$settings['rotation_enabled'] !== 1) {
        return null;
    }

    $profiles = get_profiles($pdo);
    $active = array_values(array_filter($profiles, function ($p) {
        return (int)$p['active'] === 1;
    }));

    if (empty($active)) return null;

    if ($settings['mode'] === 'random') {
        return $active[array_rand($active)];
    }

    $lastId = (int)$settings['last_profile_id'];
    $next = null;
    if ($lastId === 0) {
        $next = $active[0];
    } else {
        $foundIndex = null;
        foreach ($active as $i => $p) {
            if ((int)$p['id'] === $lastId) {
                $foundIndex = $i;
                break;
            }
        }
        if ($foundIndex === null || $foundIndex === (count($active)-1)) {
            $next = $active[0];
        } else {
            $next = $active[$foundIndex+1];
        }
    }

    if ($next) {
        $stmt = $pdo->prepare("UPDATE rotation_settings SET last_profile_id = ? WHERE id = 1");
        $stmt->execute([$next['id']]);
    }

    return $next;
}

function find_profile_for_campaign(PDO $pdo, array $campaign) {
    $from = trim($campaign['from_email'] ?? '');
    $profiles = get_profiles($pdo);

    if ($from !== '') {
        foreach ($profiles as $p) {
            if ((int)$p['active'] === 1 && strtolower($p['from_email']) === strtolower($from)) {
                return $p;
            }
        }
    }
    foreach ($profiles as $p) {
        if ((int)$p['active'] === 1) {
            return $p;
        }
    }
    return null;
}

/**
 * Check if an 'open' event exists for a campaign/recipient (safe PHP-based check)
 */
function has_open_event_for_rcpt(PDO $pdo, int $campaignId, string $rcpt): bool {
    if ($rcpt === '') return false;
    try {
        $stmt = $pdo->prepare("SELECT details FROM events WHERE campaign_id = ? AND event_type = 'open' LIMIT 2000");
        $stmt->execute([$campaignId]);
        foreach ($stmt as $row) {
            $d = json_decode($row['details'], true);
            if (is_array($d) && isset($d['rcpt']) && strtolower($d['rcpt']) === strtolower($rcpt)) {
                return true;
            }
        }
    } catch (Exception $e) {}
    return false;
}

/**
 * Buffered event logger for improved performance during high-volume sends
 * Batches event inserts to reduce database round-trips
 */
class BufferedEventLogger {
    private $pdo;
    private $buffer = [];
    private $bufferSize = 50; // Insert every 50 events
    private $campaignId;
    
    public function __construct(PDO $pdo, int $campaignId, int $bufferSize = 50) {
        $this->pdo = $pdo;
        $this->campaignId = $campaignId;
        $this->bufferSize = max(1, $bufferSize);
    }
    
    /**
     * Add an event to the buffer
     */
    public function log(string $eventType, array $details) {
        $this->buffer[] = [
            'campaign_id' => $this->campaignId,
            'event_type' => $eventType,
            'details' => json_encode($details)
        ];
        
        // Flush if buffer is full
        if (count($this->buffer) >= $this->bufferSize) {
            $this->flush();
        }
    }
    
    /**
     * Flush all buffered events to database
     */
    public function flush() {
        if (empty($this->buffer)) {
            return;
        }
        
        try {
            // Build batch insert
            $values = [];
            $params = [];
            foreach ($this->buffer as $event) {
                $values[] = "(?, ?, ?)";
                $params[] = $event['campaign_id'];
                $params[] = $event['event_type'];
                $params[] = $event['details'];
            }
            
            $sql = "INSERT INTO events (campaign_id, event_type, details) VALUES " . implode(", ", $values);
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute($params);
            
            // Clear buffer
            $this->buffer = [];
        } catch (Exception $e) {
            error_log("BufferedEventLogger flush error: " . $e->getMessage());
            // Don't throw - we don't want to stop sending if logging fails
        }
    }
    
    /**
     * Destructor ensures buffer is flushed
     */
    public function __destruct() {
        $this->flush();
    }
}

/**
 * Main send function — unchanged semantics but tolerant to forced profile and structured results from senders.
 *
 * Added $profileOverrides parameter (associative array) that may contain per-profile runtime overrides:
 * e.g. ['send_rate' => <messages_per_second>]
 */
function send_campaign_real(PDO $pdo, array $campaign, string $recipientsText, bool $isTest = false, ?int $forceProfileId = null, array $profileOverrides = [])
{
    // CRITICAL: Acquire send lock to prevent fetch operations from running simultaneously
    // This ensures separation of fetch and send as per requirements
    $sendLock = null;
    if (!$isTest) {
        $sendLock = acquire_operation_lock('send', SEND_LOCK_TIMEOUT);
        if ($sendLock === false) {
            error_log("Campaign send aborted: Could not acquire send lock (fetch operation may be running)");
            return;
        }
        error_log("Send lock acquired for campaign " . ($campaign['id'] ?? 'unknown'));
    }
    
    try {
        $recipients = array_filter(array_map('trim', preg_split("/\r\n|\n|\r|,/", $recipientsText)));
        if (empty($recipients)) {
            // Lock will be released by finally block
            return;
        }

        $rotSettings      = get_rotation_settings($pdo);
        $rotationEnabled  = (int)$rotSettings['rotation_enabled'] === 1;

        $total = count($recipients);
        if (!$isTest) {
            try {
                $stmt = $pdo->prepare("UPDATE campaigns SET status='sending', total_recipients=? WHERE id=?");
                $stmt->execute([$total, $campaign['id']]);
                // Initialize progress tracking
                update_campaign_progress($pdo, $campaign['id'], 0, $total, 'sending');
            } catch (Exception $e) {}
        }

        $ins = $pdo->prepare("INSERT INTO events (campaign_id, event_type, details) VALUES (?,?,?)");

        $ok = 0;
        $failed = 0;

        $attempted = [];

    foreach ($recipients as $email) {
        $emailLower = strtolower(trim($email));
        if ($emailLower === '') continue;

        if (in_array($emailLower, $attempted, true)) {
            continue;
        }
        $attempted[] = $emailLower;

        try {
            if (is_unsubscribed($pdo, $emailLower)) {
                $ins->execute([
                    $campaign['id'],
                    'skipped_unsubscribe',
                    json_encode([
                        'rcpt' => $emailLower,
                        'reason' => 'recipient in unsubscribes table',
                        'test' => $isTest ? 1 : 0,
                    ])
                ]);
                $failed++;
                // increment sends_used? No - we didn't attempt to send.
                continue;
            }

            if ($forceProfileId !== null) {
                $stmtpf = $pdo->prepare("SELECT * FROM sending_profiles WHERE id = ? LIMIT 1");
                $stmtpf->execute([$forceProfileId]);
                $profile = $stmtpf->fetch(PDO::FETCH_ASSOC);
                if (!$profile) {
                    throw new Exception("Forced profile not found: {$forceProfileId}");
                }
                // Apply runtime overrides if provided (e.g., send_rate)
                if (!empty($profileOverrides) && isset($profileOverrides['send_rate'])) {
                    $profile['send_rate'] = (int)$profileOverrides['send_rate'];
                }
            } else {
                if ($rotationEnabled) {
                    $profile = pick_next_profile($pdo);
                } else {
                    $profile = find_profile_for_campaign($pdo, $campaign);
                }
            }

            if (!$profile) {
                throw new Exception("No active sending profile configured.");
            }

            // Refresh sends_used & max_sends from DB to ensure we don't exceed limit (protect concurrent runs)
            if (!empty($profile['id'])) {
                try {
                    $stmtSU = $pdo->prepare("SELECT COALESCE(sends_used,0) as su FROM sending_profiles WHERE id = ? LIMIT 1");
                    $stmtSU->execute([$profile['id']]);
                    $profile['sends_used'] = (int)$stmtSU->fetchColumn();
                } catch (Exception $e) {
                    $profile['sends_used'] = (int)($profile['sends_used'] ?? 0);
                }
            } else {
                $profile['sends_used'] = 0;
            }

            $maxSends = max(0, (int)($profile['max_sends'] ?? 0));
            if ($maxSends > 0 && $profile['sends_used'] >= $maxSends) {
                // profile exhausted - record skipped event and continue
                $ins->execute([
                    $campaign['id'],
                    'skipped_max_sends',
                    json_encode([
                        'rcpt' => $emailLower,
                        'profile_id' => $profile['id'] ?? null,
                        'reason' => 'profile reached max_sends',
                        'test' => $isTest ? 1 : 0,
                    ])
                ]);
                $failed++;
                continue;
            }

            // Resolve FROM address:
            // - If a profile is forced (worker per-profile) --> always use that profile's from_email.
            // - Else if rotation is enabled --> use the profile's from_email.
            // - Else (rotation disabled) --> prefer campaign's from_email, fallback to profile's from_email.
            $fromToUse = '';
            if ($forceProfileId !== null) {
                // Forced per-profile send: use profile's from
                $fromToUse = trim($profile['from_email'] ?? '');
            } elseif ($rotationEnabled) {
                // Global rotation enabled: each profile should send with its own From
                $fromToUse = trim($profile['from_email'] ?? '');
            } else {
                // Rotation disabled: keep the campaign-level From if provided, else fallback to profile
                $fromToUse = trim($campaign['from_email'] ?? '');
                if ($fromToUse === '') {
                    $fromToUse = trim($profile['from_email'] ?? '');
                }
            }

            if ($fromToUse === '') {
                throw new Exception("No FROM address resolved for this send.");
            }

            $campaignSend = $campaign;
            $campaignSend['from_email'] = $fromToUse;

            $campaignSend['sender_name'] = trim($profile['sender_name'] ?? '');
            if ($campaignSend['sender_name'] === '') {
                $campaignSend['sender_name'] = trim($campaign['sender_name'] ?? '');
            }

            $htmlTracked = build_tracked_html($campaignSend, $emailLower);

            // Respect overrides for send_rate (profileOverrides wins, then profile setting)
            $effectiveSendRate = 0;
            if (!empty($profileOverrides) && isset($profileOverrides['send_rate'])) {
                $effectiveSendRate = (int)$profileOverrides['send_rate'];
            } elseif (!empty($profile['send_rate'])) {
                $effectiveSendRate = (int)$profile['send_rate'];
            } else {
                $effectiveSendRate = 0;
            }

            if (isset($profile['type']) && $profile['type'] === 'api') {
                $res = api_send_mail($profile, $campaignSend, $emailLower, $htmlTracked);
            } else {
                $res = smtp_send_mail($profile, $campaignSend, $emailLower, $htmlTracked);
            }

            // After attempting send, increment sends_used (persist)
            if (!empty($profile['id'])) {
                try {
                    $stmtInc = $pdo->prepare("UPDATE sending_profiles SET sends_used = COALESCE(sends_used,0) + 1 WHERE id = ?");
                    $stmtInc->execute([$profile['id']]);
                } catch (Exception $ex) {
                    // ignore
                }
            }

            if (!is_array($res)) {
                $ins->execute([
                    $campaign['id'],
                    'bounce',
                    json_encode([
                        'rcpt'  => $emailLower,
                        'error' => 'Invalid send function response',
                        'profile_id' => isset($profile['id']) ? $profile['id'] : null,
                        'via'   => isset($profile['type']) ? $profile['type'] : null,
                        'test'  => $isTest ? 1 : 0,
                        'mode'  => 'sync',
                    ])
                ]);
                add_to_unsubscribes($pdo, $emailLower);
                try {
                    if (!empty($profile) && isset($profile['id']) && ($profile['type'] ?? '') === 'smtp') {
                        $stmt = $pdo->prepare("UPDATE sending_profiles SET active = 0 WHERE id = ?");
                        $stmt->execute([$profile['id']]);
                        $ins->execute([
                            $campaign['id'],
                            'profile_disabled',
                            json_encode([
                                'profile_id' => $profile['id'],
                                'reason' => 'invalid send function response',
                                'test' => $isTest ? 1 : 0,
                            ])
                        ]);
                    }
                } catch (Exception $ex) {}
                $failed++;
                continue;
            }

            if (!empty($res['ok']) && ($res['type'] ?? '') === 'delivered') {
                $ins->execute([
                    $campaign['id'],
                    'delivered',
                    json_encode([
                        'rcpt'       => $emailLower,
                        'profile_id' => $profile['id'] ?? null,
                        'via'        => $profile['type'] ?? null,
                        'smtp_code'  => $res['code'] ?? null,
                        'smtp_msg'   => $res['msg'] ?? '',
                        'stage'      => $res['stage'] ?? 'done',
                        'test'       => $isTest ? 1 : 0,
                        'mode'       => 'sync',
                    ])
                ]);
                $ok++;
            } else {
                $etype = ($res['type'] === 'deferred') ? 'deferred' : 'bounce';

                $ins->execute([
                    $campaign['id'],
                    $etype,
                    json_encode([
                        'rcpt'       => $emailLower,
                        'profile_id' => $profile['id'] ?? null,
                        'via'        => $profile['type'] ?? null,
                        'smtp_code'  => $res['code'] ?? null,
                        'smtp_msg'   => $res['msg'] ?? '',
                        'stage'      => $res['stage'] ?? 'unknown',
                        'test'       => $isTest ? 1 : 0,
                        'mode'       => 'sync',
                        'log'        => $res['log'] ?? [],
                    ])
                ]);

                if ($etype === 'bounce') {
                    add_to_unsubscribes($pdo, $emailLower);
                }

                try {
                    if (!empty($profile) && isset($profile['id']) && ($profile['type'] ?? '') === 'smtp' && $etype === 'bounce') {
                        $stmt = $pdo->prepare("UPDATE sending_profiles SET active = 0 WHERE id = ?");
                        $stmt->execute([$profile['id']]);

                        $ins->execute([
                            $campaign['id'],
                            'profile_disabled',
                            json_encode([
                                'profile_id' => $profile['id'],
                                'reason' => 'bounce/connection failure during send',
                                'test' => $isTest ? 1 : 0,
                            ])
                        ]);
                    }
                } catch (Exception $ex) { /* ignore */ }

                $failed++;
            }
        } catch (Exception $e) {
            $ins->execute([
                $campaign['id'],
                'bounce',
                json_encode([
                    'rcpt'        => $emailLower,
                    'error'       => $e->getMessage(),
                    'profile_id'  => isset($profile['id']) ? $profile['id'] : null,
                    'via'         => isset($profile['type']) ? $profile['type'] : null,
                    'test'        => $isTest ? 1 : 0,
                    'mode'        => 'exception',
                ])
            ]);
            add_to_unsubscribes($pdo, $emailLower);

            try {
                if (!empty($profile) && isset($profile['id']) && ($profile['type'] ?? '') === 'smtp') {
                    $stmt = $pdo->prepare("UPDATE sending_profiles SET active = 0 WHERE id = ?");
                    $stmt->execute([$profile['id']]);
                }
            } catch (Exception $ex) {}

            // increment sends_used even on exception to count attempt
            if (!empty($profile['id'])) {
                try {
                    $stmtInc = $pdo->prepare("UPDATE sending_profiles SET sends_used = COALESCE(sends_used,0) + 1 WHERE id = ?");
                    $stmtInc->execute([$profile['id']]);
                } catch (Exception $_) {}
            }

            $failed++;
        }

        // Throttle according to effective send rate (overrides or profile)
        $sendRate = 0;
        if (!empty($profileOverrides) && isset($profileOverrides['send_rate'])) {
            $sendRate = (int)$profileOverrides['send_rate'];
        } elseif (!empty($profile) && isset($profile['send_rate'])) {
            $sendRate = (int)$profile['send_rate'];
        } else {
            $sendRate = 0;
        }

        if ($sendRate > 0) {
            $micro = (int)(1000000 / max(1, $sendRate));
            if ($micro > 0) {
                usleep($micro);
            }
        }
        
        // Update progress periodically (every 10 emails)
        if (!$isTest && ($ok + $failed) % PROGRESS_UPDATE_FREQUENCY === 0) {
            update_campaign_progress($pdo, $campaign['id'], $ok + $failed, $total, 'sending');
        }
    }

    // Final progress and status update - CRITICAL: This must always execute
    if (!$isTest) {
        // Mark progress as completed
        update_campaign_progress($pdo, $campaign['id'], $ok + $failed, $total, 'completed');
        
        // Update main campaign status to 'sent'
        try {
            $stmt = $pdo->prepare("UPDATE campaigns SET status='sent', sent_at = COALESCE(sent_at, NOW()) WHERE id=?");
            $result = $stmt->execute([$campaign['id']]);
            if (!$result) {
                error_log("Failed to update campaign status to sent for campaign ID: {$campaign['id']}");
                // Fallback: retry status update with fresh prepared statement
                $fallbackStmt = $pdo->prepare("UPDATE campaigns SET status='sent', sent_at = COALESCE(sent_at, NOW()) WHERE id=?");
                $fallbackStmt->execute([$campaign['id']]);
            } else {
                error_log("Successfully updated campaign {$campaign['id']} status to sent (simple mode)");
            }
        } catch (Exception $e) {
            error_log("Exception updating campaign status to sent (simple): " . $e->getMessage());
            // Final fallback: attempt one last status update
            try {
                $fallbackStmt = $pdo->prepare("UPDATE campaigns SET status='sent', sent_at = COALESCE(sent_at, NOW()) WHERE id=?");
                $fallbackStmt->execute([$campaign['id']]);
                error_log("Fallback status update successful for campaign {$campaign['id']}");
            } catch (Exception $e2) {
                error_log("Fallback status update also failed: " . $e2->getMessage());
            }
        }
    }
    } finally {
        // CRITICAL: Always release the send lock, even if an error occurs
        if ($sendLock) {
            release_operation_lock($sendLock, 'send');
            error_log("Send lock released for campaign " . ($campaign['id'] ?? 'unknown'));
        }
    }
}

///////////////////////
//  CONCURRENT EMAIL SENDING ARCHITECTURE
///////////////////////
/**
 * CONCURRENT EMAIL SENDING SYSTEM
 * ================================
 * 
 * This system implements high-performance concurrent email sending using PHP's pcntl extension
 * for process forking. When pcntl is not available, it falls back to sequential processing.
 * 
 * ARCHITECTURE OVERVIEW:
 * 
 * 1. MULTI-LEVEL PARALLELIZATION
 *    - Level 1: Profile-level parallelization (rotation mode)
 *      When rotation is enabled with multiple profiles, each profile gets a separate process
 *    - Level 2: Connection-level parallelization (within each profile)
 *      Each profile spawns N workers based on connection_numbers setting
 *    - Level 3: Batch-level persistent connections (within each worker)
 *      Each worker sends batch_size emails before reconnecting to SMTP
 * 
 * 2. PROCESS HIERARCHY
 *    Main Process
 *    ├── Profile Worker 1 (if rotation enabled)
 *    │   ├── Connection Worker 1
 *    │   ├── Connection Worker 2
 *    │   └── Connection Worker N
 *    ├── Profile Worker 2
 *    │   └── ...
 *    └── Profile Worker M
 * 
 * 3. KEY FUNCTIONS
 *    - send_campaign_with_rotation(): Entry point for multi-profile campaigns
 *    - send_campaign_with_connections(): Entry point for single-profile campaigns
 *    - send_campaign_with_connections_for_profile_concurrent(): Spawns connection workers
 *    - process_worker_batch(): Worker function that runs in forked process
 * 
 * 4. CONFIGURATION (Optimized for 128GB RAM high-performance servers)
 *    - connection_numbers: Number of concurrent SMTP connections (1-100, default 20, optimized from 5)
 *    - batch_size: Emails per connection before reconnecting (1-1000, default 200, optimized from 50)
 *    - rotation_enabled: Use multiple profiles concurrently
 *    - workers: Number of parallel workers per profile (default 16, auto-calculated as 16 per 1,000 emails)
 * 
 * 5. THREAD SAFETY
 *    - Each worker creates its own database connection
 *    - Workers log events independently to avoid contention
 *    - Parent processes count results via database queries
 *    - Worker PIDs tracked in event details for debugging
 * 
 * 6. PERFORMANCE CHARACTERISTICS
 *    Example: 10,000 emails, 2 profiles, 10 workers per profile, batch size 50
 *    - 2 profile processes (parallel)
 *    - Each profile spawns 10 workers (parallel within profile)
 *    - Total: 20 concurrent workers in this example (2 profiles × 10 workers)
 *    - Each worker processes emails in batches
 *    - Worker count is unlimited - can be scaled based on system resources
 *    - Theoretical: ~Nx speedup vs sequential (where N = total workers)
 *    - Actual speedup limited by: system resources (CPU, memory), SMTP server limits,
 *      network capacity, and PHP process management overhead
 * 
 * 7. FALLBACK BEHAVIOR
 *    - If pcntl_fork unavailable: Sequential processing with same batch optimization
 *    - If fork fails: Logs error and continues with remaining workers
 *    - Test mode: Always sequential for predictability
 */

/**
 * Worker function to process a batch of emails in a separate process
 * This runs in a forked child process and exits when done
 * 
 * CRITICAL CONNECTION MANAGEMENT (per requirements):
 * - Creates its own LOCAL database connection (never passed from parent)
 * - Explicitly closes connection with $pdo = null before exit
 * - Does NOT accept PDO as parameter - reconstructs from config
 * - Uses short timeouts (20-30 seconds) via DB_OPERATION_TIMEOUT and DB_SESSION_TIMEOUT
 * 
 * @param array $dbConfig Database configuration
 * @param array $profile SMTP profile
 * @param array $campaign Campaign data
 * @param array $recipients Recipients for this worker
 * @param bool $isTest Whether this is a test send
 */
function process_worker_batch(array $dbConfig, array $profile, array $campaign, array $recipients, bool $isTest = false) {
    $workerId = getmypid();
    $assignedCount = count($recipients);
    error_log("Worker $workerId starting with $assignedCount recipients assigned");
    
    // CRITICAL FIX: Create new PDO connection for this worker process with retry logic
    // REQUIREMENT: Each worker creates its own LOCAL connection, never passed from parent
    // Enhanced retry with specific handling for "too many connections"
    $maxRetries = 5;
    $pdo = null;
    for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
        try {
            $pdo = new PDO(
                "mysql:host={$dbConfig['host']};dbname={$dbConfig['name']};charset=utf8mb4",
                $dbConfig['user'],
                $dbConfig['pass'],
                [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    // REQUIREMENT: Short timeouts (20 seconds for operations)
                    PDO::ATTR_TIMEOUT => DB_OPERATION_TIMEOUT,
                    // REQUIREMENT: Non-persistent connections to prevent accumulation
                    PDO::ATTR_PERSISTENT => false,
                    // REQUIREMENT: Short server-side session timeout (20 seconds)
                    PDO::MYSQL_ATTR_INIT_COMMAND => "SET SESSION wait_timeout=" . DB_SESSION_TIMEOUT . ", SESSION interactive_timeout=" . DB_SESSION_TIMEOUT
                ]
            );
            error_log("Worker $workerId: Database connection established (attempt $attempt)");
            break;
        } catch (Exception $e) {
            $errorMsg = $e->getMessage();
            $isTooManyConnections = (strpos($errorMsg, '1040') !== false || strpos($errorMsg, 'Too many connections') !== false);
            
            if ($isTooManyConnections) {
                // CRITICAL: Database connection pool exhausted
                error_log("Worker $workerId: DATABASE CONNECTION POOL EXHAUSTED (attempt $attempt/$maxRetries)");
                error_log("Worker $workerId: This indicates MySQL max_connections is too low or too many workers are spawning");
                error_log("Worker $workerId: Current limit exceeded - waiting longer before retry");
            } else {
                error_log("Worker $workerId: DB connection error (attempt $attempt/$maxRetries): " . $errorMsg);
            }
            
            if ($attempt >= $maxRetries) {
                error_log("Worker $workerId: FAILED to connect after $maxRetries attempts. Exiting.");
                if ($isTooManyConnections) {
                    error_log("Worker $workerId: SOLUTION: Increase MySQL max_connections in my.cnf or reduce MAX_CONCURRENT_WORKERS");
                }
                exit(1);
            }
            
            // Exponential backoff with longer delays for connection exhaustion
            // For "too many connections", wait significantly longer to allow other workers to finish
            if ($isTooManyConnections) {
                $backoffSeconds = 10 * $attempt + random_int(0, 5); // 10s, 20s, 30s, 40s, 50s + jitter
            } else {
                $backoffSeconds = 3 * $attempt + random_int(0, 2); // 3s, 6s, 9s, 12s, 15s + jitter
            }
            
            error_log("Worker $workerId: Waiting {$backoffSeconds}s before retry...");
            sleep($backoffSeconds);
        }
    }
    
    $campaignId = (int)$campaign['id'];
    $ins = $pdo->prepare("INSERT INTO events (campaign_id, event_type, details) VALUES (?,?,?)");
    
    // CRITICAL FIX: Log worker start event for better tracking
    // This helps identify when workers are actually running vs appearing as "0 workers"
    try {
        $ins->execute([
            $campaignId,
            'worker_start',
            json_encode([
                'worker_pid' => $workerId,
                'assigned_count' => $assignedCount,
                'profile_id' => $profile['id'] ?? null,
            ])
        ]);
    } catch (Exception $e) {
        error_log("Worker $workerId: Failed to log start event: " . $e->getMessage());
    }
    
    $sent = 0;
    $failed = 0;
    $processedCount = 0;
    $lastProgressUpdate = 0; // Track when we last updated progress
    $lastHeartbeat = 0; // Track when we last sent a heartbeat
    
    // Prepare campaign for sending
    $campaignSend = $campaign;
    $campaignSend['from_email'] = trim($profile['from_email'] ?? $campaign['from_email']);
    $campaignSend['sender_name'] = trim($profile['sender_name'] ?? $campaign['sender_name']);
    
    // Process all recipients assigned to this worker
    // Build HTML map for each recipient
    $htmlMap = [];
    foreach ($recipients as $emailLower) {
        $htmlMap[$emailLower] = build_tracked_html($campaignSend, $emailLower);
    }
    
    try {
        if (isset($profile['type']) && $profile['type'] === 'api') {
            // API batch sending – groups recipients into batches of API_RECIPIENTS_PER_REQUEST,
            // delivered via curl_multi pool with HTTP/2 and persistent connections.
            // Events are inserted in real time via $onBatchComplete (fired per curl_multi batch,
            // before $onProgress), so 'delivered' count tracks with progress_sent during sending.
            $callbackCredited = 0;
            $workerPid = getmypid();
            api_send_batch($profile, $campaignSend, $recipients, $htmlMap,
                // $onProgress: update progress_sent after events are already inserted
                !$isTest ? function(int $delta) use ($campaignId, $workerId, &$progressStmt, &$callbackCredited, $pdo) {
                    $callbackCredited += $delta;
                    try {
                        if (!isset($progressStmt)) {
                            $progressStmt = $pdo->prepare("UPDATE campaigns SET progress_sent = progress_sent + ? WHERE id = ?");
                        }
                        $progressStmt->execute([$delta, $campaignId]);
                    } catch (Exception $e) {
                        error_log("Worker $workerId: API progress callback error: " . $e->getMessage());
                    }
                } : null,
                // $onBatchComplete: insert event rows immediately so delivered updates during send
                function(array $batchEmails, array $batchResults) use (
                    $campaignId, $profile, $isTest, $workerPid,
                    $ins, $pdo, &$sent, &$failed, &$processedCount, &$lastHeartbeat
                ) {
                    foreach ($batchEmails as $emailLower) {
                        $res = $batchResults[$emailLower] ?? [
                            'ok' => false, 'type' => 'deferred', 'code' => null, 'msg' => 'No batch result',
                        ];
                        if (!empty($res['ok']) && ($res['type'] ?? '') === 'delivered') {
                            $ins->execute([
                                $campaignId,
                                'delivered',
                                json_encode([
                                    'rcpt' => $emailLower,
                                    'profile_id' => $profile['id'] ?? null,
                                    'via' => 'worker_' . ($profile['type'] ?? 'api'),
                                    'smtp_code' => $res['code'] ?? null,
                                    'test' => $isTest ? 1 : 0,
                                    'worker_pid' => $workerPid,
                                ])
                            ]);
                            $sent++;
                        } else {
                            $etype = ($res['type'] === 'deferred') ? 'deferred' : 'bounce';
                            $ins->execute([
                                $campaignId,
                                $etype,
                                json_encode([
                                    'rcpt' => $emailLower,
                                    'profile_id' => $profile['id'] ?? null,
                                    'via' => 'worker_' . ($profile['type'] ?? 'api'),
                                    'smtp_code' => $res['code'] ?? null,
                                    'smtp_msg' => $res['msg'] ?? '',
                                    'test' => $isTest ? 1 : 0,
                                    'worker_pid' => $workerPid,
                                ])
                            ]);
                            if ($etype === 'bounce') {
                                add_to_unsubscribes($pdo, $emailLower);
                            }
                            $failed++;
                        }
                        $processedCount++;
                        // Log heartbeat periodically to show worker is still active
                        if ($processedCount - $lastHeartbeat >= WORKER_HEARTBEAT_FREQUENCY) {
                            try {
                                $ins->execute([
                                    $campaignId,
                                    'worker_heartbeat',
                                    json_encode([
                                        'worker_pid' => $workerPid,
                                        'processed' => $processedCount,
                                        'sent' => $sent,
                                        'failed' => $failed,
                                    ])
                                ]);
                                $lastHeartbeat = $processedCount;
                            } catch (Exception $e) {}
                        }
                    }
                });
            // Set $lastProgressUpdate so the final-remainder block does not double-count
            $lastProgressUpdate = $callbackCredited;
        } else {
            // SMTP batch sending with persistent connection.
            // Progress is reported DURING sending via an inline callback fired every
            // SMTP_PROGRESS_CHUNK_SIZE emails — no reconnection overhead.
            // With 117 parallel workers each reporting every 50 emails the UI sees
            // 117 × 50 = 5,850 new emails per callback round, well above the 5,000/s target.
            $callbackCredited = 0;
            $batchResult = smtp_send_batch(
                $profile,
                $campaignSend,
                $recipients,
                $htmlMap,
                function(int $delta) use ($campaignId, $workerId, $isTest, &$progressStmt, &$lastProgressUpdate, &$callbackCredited, $pdo) {
                    if ($isTest) return;
                    $callbackCredited   += $delta;
                    $lastProgressUpdate += $delta;
                    try {
                        if (!isset($progressStmt)) {
                            $progressStmt = $pdo->prepare("UPDATE campaigns SET progress_sent = progress_sent + ? WHERE id = ?");
                        }
                        $progressStmt->execute([$delta, $campaignId]);
                    } catch (Exception $e) {
                        error_log("Worker $workerId: Inline progress update error: " . $e->getMessage());
                    }
                },
                SMTP_PROGRESS_CHUNK_SIZE
            );
            
            // Credit any remainder not yet covered by callbacks
            // (e.g., last partial chunk when count(results) % SMTP_PROGRESS_CHUNK_SIZE != 0)
            if (!$isTest) {
                $resultCount = count($batchResult['results'] ?? []);
                $remaining   = $resultCount - $callbackCredited;
                if ($remaining > 0) {
                    try {
                        if (!isset($progressStmt)) {
                            $progressStmt = $pdo->prepare("UPDATE campaigns SET progress_sent = progress_sent + ? WHERE id = ?");
                        }
                        $progressStmt->execute([$remaining, $campaignId]);
                        $lastProgressUpdate += $remaining;
                    } catch (Exception $e) {
                        error_log("Worker $workerId: Remainder progress update error: " . $e->getMessage());
                    }
                }
            }
            
            if (isset($batchResult['results']) && is_array($batchResult['results'])) {
                foreach ($batchResult['results'] as $recipientEmail => $res) {
                    if (!empty($res['ok']) && ($res['type'] ?? '') === 'delivered') {
                        $ins->execute([
                            $campaignId,
                            'delivered',
                            json_encode([
                                'rcpt' => $recipientEmail,
                                'profile_id' => $profile['id'] ?? null,
                                'via' => 'worker_smtp',
                                'smtp_code' => $res['code'] ?? null,
                                'test' => $isTest ? 1 : 0,
                                'worker_pid' => getmypid(),
                            ])
                        ]);
                        $sent++;
                    } else {
                        $etype = ($res['type'] === 'deferred') ? 'deferred' : 'bounce';
                        $ins->execute([
                            $campaignId,
                            $etype,
                            json_encode([
                                'rcpt' => $recipientEmail,
                                'profile_id' => $profile['id'] ?? null,
                                'via' => 'worker_smtp',
                                'smtp_code' => $res['code'] ?? null,
                                'smtp_msg' => $res['msg'] ?? '',
                                'test' => $isTest ? 1 : 0,
                                'worker_pid' => getmypid(),
                            ])
                        ]);
                        if ($etype === 'bounce') {
                            add_to_unsubscribes($pdo, $recipientEmail);
                        }
                        $failed++;
                    }
                    
                    // Update processedCount for heartbeat tracking (progress is handled by the callback above)
                    $processedCount++;
                    
                    // Log heartbeat periodically to show worker is still active
                    if ($processedCount - $lastHeartbeat >= WORKER_HEARTBEAT_FREQUENCY) {
                        try {
                            $ins->execute([
                                $campaignId,
                                'worker_heartbeat',
                                json_encode([
                                    'worker_pid' => getmypid(),
                                    'processed' => $processedCount,
                                    'sent' => $sent,
                                    'failed' => $failed,
                                ])
                            ]);
                            $lastHeartbeat = $processedCount;
                        } catch (Exception $e) {}
                    }
                }
            } else {
                // Batch failed entirely
                foreach ($recipients as $emailLower) {
                    $ins->execute([
                        $campaignId,
                        'bounce',
                        json_encode([
                            'rcpt' => $emailLower,
                            'error' => $batchResult['error'] ?? 'Worker connection failed',
                            'profile_id' => $profile['id'] ?? null,
                            'via' => 'worker_smtp',
                            'test' => $isTest ? 1 : 0,
                            'worker_pid' => getmypid(),
                        ])
                    ]);
                    add_to_unsubscribes($pdo, $emailLower);
                    $failed++;
                }
            }
        }
    } catch (Exception $e) {
        $workerId = getmypid();
        error_log("Worker $workerId: CRITICAL EXCEPTION: " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine());
        error_log("Worker $workerId: Stack trace: " . $e->getTraceAsString());
        
        // Try to log the failure for remaining recipients
        try {
            foreach ($recipients as $emailLower) {
                $ins->execute([
                    $campaignId,
                    'bounce',
                    json_encode([
                        'rcpt' => $emailLower,
                        'error' => 'Worker exception: ' . $e->getMessage(),
                        'profile_id' => $profile['id'] ?? null,
                        'test' => $isTest ? 1 : 0,
                        'worker_pid' => getmypid(),
                    ])
                ]);
                add_to_unsubscribes($pdo, $emailLower);
                $failed++;
            }
        } catch (Exception $innerE) {
            error_log("Worker $workerId: Failed to log bounces: " . $innerE->getMessage());
        }
    }
    
    // Final progress update for any remaining emails not yet reported
    if (!$isTest && $processedCount > $lastProgressUpdate) {
        $remaining = $processedCount - $lastProgressUpdate;
        try {
            $stmt = $pdo->prepare("UPDATE campaigns SET progress_sent = progress_sent + ? WHERE id = ?");
            $stmt->execute([$remaining, $campaignId]);
        } catch (Exception $e) {
            error_log("Worker $workerId: Final progress update error: " . $e->getMessage());
        }
    }
    
    // Log completion stats
    error_log("Worker $workerId completed: assigned=$assignedCount, processed=$processedCount, sent=$sent, failed=$failed");
    if ($processedCount != $assignedCount) {
        error_log("Worker $workerId WARNING: Processed $processedCount but was assigned $assignedCount recipients!");
    }
    
    // CRITICAL FIX: Log worker completion event for better tracking
    try {
        $ins->execute([
            $campaignId,
            'worker_complete',
            json_encode([
                'worker_pid' => $workerId,
                'assigned' => $assignedCount,
                'processed' => $processedCount,
                'sent' => $sent,
                'failed' => $failed,
            ])
        ]);
    } catch (Exception $e) {
        error_log("Worker $workerId: Failed to log complete event: " . $e->getMessage());
    }
    
    // Explicitly close prepared statements and database connection before worker exit to prevent connection leaks
    // REQUIREMENT: Manual connection closing with $pdo = null to prevent accumulation
    try {
        // Close prepared statements first (release any locks/resources)
        if (isset($progressStmt)) {
            $progressStmt = null;
        }
        if (isset($ins)) {
            $ins = null;
        }
        if (isset($stmt)) {
            $stmt = null;
        }
        // CRITICAL: Close database connection explicitly
        // REQUIREMENT: $pdo = null to ensure connection is released back to pool
        $pdo = null;
        error_log("Worker $workerId: Database connection closed successfully (REQUIREMENT: explicit $pdo = null)");
    } catch (Exception $e) {
        error_log("Worker $workerId: Error closing connection: " . $e->getMessage());
    }
    
    exit(0);
}

/**
 * Concurrent version of send_campaign_with_connections_for_profile
 * Uses process forking to send via multiple concurrent connections
 */
function send_campaign_with_connections_for_profile_concurrent(PDO $pdo, array $campaign, array $recipients, array $profile, bool $isTest = false) {
    global $DB_HOST, $DB_NAME, $DB_USER, $DB_PASS;
    
    if (empty($recipients)) {
        return ['sent' => 0, 'failed' => 0];
    }
    
    $campaignId = (int)$campaign['id'];
    
    // Count recipients for auto-calculation
    $totalRecipients = count($recipients);
    
    // Read messages_per_worker FIRST — this drives the worker count calculation.
    // Cap at DEFAULT_MESSAGES_PER_WORKER so a stale/large DB value cannot shrink the
    // number of workers that actually get forked (old DB rows may still hold 1000 or 2000).
    $messagesPerWorker = resolve_messages_per_worker((int)($profile['messages_per_worker'] ?? DEFAULT_MESSAGES_PER_WORKER));
    
    // Compute worker count from the SAME divisor used in the distribution loop below.
    // Using calculate_workers_from_count() (which divides by DEFAULT_MESSAGES_PER_WORKER)
    // while then distributing with a DIFFERENT chunk size is the hidden bug that causes
    // "117 slots allocated, only 59 filled".
    $autoWorkers = max(MIN_WORKERS, min(MAX_WORKERS, intdiv($totalRecipients, $messagesPerWorker)));
    // Enforce MAX_WORKERS cap on profile settings for resource safety
    $profileWorkers = max(MIN_WORKERS, min(MAX_WORKERS, (int)($profile['workers'] ?? DEFAULT_WORKERS)));
    
    // Use the higher of auto-calculated or profile-configured workers (both already capped)
    $workers = max($autoWorkers, $profileWorkers);
    
    // CRITICAL LOGGING: Track worker calculation
    error_log("Campaign $campaignId WORKER CALC: total=$totalRecipients, mpw=$messagesPerWorker, auto=$autoWorkers, profile=$profileWorkers, final=$workers");
    
    // Log info about worker calculation for large campaigns only (>= 5000 emails)
    if ($totalRecipients >= 5000) {
        error_log("Campaign $campaignId (concurrent): $totalRecipients emails → $autoWorkers workers × $messagesPerWorker emails/worker (profile setting: $profileWorkers, using: $workers)");
    }
    
    // Log info for large worker counts (informational only)
    if ($workers >= WORKERS_WARNING_THRESHOLD) {
        error_log("Info: Profile {$profile['id']} using $workers workers for campaign $campaignId (high throughput mode with resource safety cap)");
    }
    
    $ins = $pdo->prepare("INSERT INTO events (campaign_id, event_type, details) VALUES (?,?,?)");
    
    // Filter out unsubscribed recipients.
    // Use a hash map for O(1) duplicate detection instead of in_array (which is O(N),
    // making the whole loop O(N²) for large lists).
    $validRecipients = [];
    $seenEmails = [];
    $failed = 0;
    foreach ($recipients as $email) {
        $emailLower = strtolower(trim($email));
        if ($emailLower === '' || isset($seenEmails[$emailLower])) {
            continue;
        }
        $seenEmails[$emailLower] = true;
        
        if (is_unsubscribed($pdo, $emailLower)) {
            $ins->execute([
                $campaignId,
                'skipped_unsubscribe',
                json_encode([
                    'rcpt' => $emailLower,
                    'reason' => 'unsubscribed',
                    'test' => $isTest ? 1 : 0,
                ])
            ]);
            $failed++;
            continue;
        }
        
        $validRecipients[] = $emailLower;
    }
    unset($seenEmails);
    
    // Divide recipients across workers using round-robin with messagesPerWorker as batch size
    $totalValidRecipients = count($validRecipients);
    if ($totalValidRecipients === 0) {
        return ['sent' => 0, 'failed' => $failed];
    }
    
    // Spawn up to $workers parallel processes (but not more than total recipients)
    $actualWorkers = min($workers, $totalValidRecipients);
    
    // Distribute recipients using round-robin with messagesPerWorker as batch size
    // This matches the spawn_parallel_workers logic
    $workerRecipients = array_fill(0, $actualWorkers, []);
    $cycleDelayMs = (int)($profile['cycle_delay_ms'] ?? DEFAULT_CYCLE_DELAY_MS);
    
    $recipientIdx = 0;
    $roundCount = 0;
    while ($recipientIdx < $totalValidRecipients) {
        // No cycle delay during queued phase - optimize for speed
        
        for ($workerIdx = 0; $workerIdx < $actualWorkers && $recipientIdx < $totalValidRecipients; $workerIdx++) {
            $chunkSize = min($messagesPerWorker, $totalValidRecipients - $recipientIdx);
            $chunk = array_slice($validRecipients, $recipientIdx, $chunkSize);
            // Use array_push with spread operator for O(n) performance instead of array_merge O(n²)
            array_push($workerRecipients[$workerIdx], ...$chunk);
            $recipientIdx += $chunkSize;
        }
        
        $roundCount++;
    }
    
    // Convert to worker batches array (filter out empty)
    $workerBatches = [];
    foreach ($workerRecipients as $recips) {
        if (!empty($recips)) {
            $workerBatches[] = $recips;
        }
    }
    
    // Check if pcntl extension is available for true concurrency
    $canFork = function_exists('pcntl_fork') && function_exists('pcntl_waitpid');
    
    if (!$canFork) {
        // Fallback to sequential processing if pcntl not available
        return send_campaign_with_connections_for_profile($pdo, $campaign, $recipients, $profile, $isTest);
    }

    // API profiles must NOT use the worker-fork mechanism. api_send_batch already provides
    // parallelism via the curl_multi pool (8 handles). Forking N workers for an API profile
    // spawns N simultaneous HTTP requests (one per worker) with no inter-request pacing,
    // hitting the API's rate limits and exhausting MySQL max_connections. Route to the
    // sequential path which calls api_send_batch once with all recipients.
    if (($profile['type'] ?? '') === 'api') {
        return send_campaign_with_connections_for_profile($pdo, $campaign, $recipients, $profile, $isTest);
    }
    
    // Prepare database config for workers
    $dbConfig = [
        'host' => $DB_HOST,
        'name' => $DB_NAME,
        'user' => $DB_USER,
        'pass' => $DB_PASS,
    ];
    
    // OPTIMIZATION: Spawn workers in batches to prevent resource exhaustion
    // Instead of forking all workers at once (which can be 500+ for large campaigns),
    // fork them in batches of MAX_CONCURRENT_WORKERS with delays between batches
    $totalWorkers = count($workerBatches);
    $batchSize = MAX_CONCURRENT_WORKERS;
    $workerBatchGroups = array_chunk($workerBatches, $batchSize, true);
    
    error_log("Campaign $campaignId (profile {$profile['id']}): Forking $totalWorkers workers in " . count($workerBatchGroups) . " batch groups of up to $batchSize workers each");
    
    $allWorkerPids = [];
    
    foreach ($workerBatchGroups as $batchGroupIdx => $batchGroup) {
        $batchPids = [];
        
        foreach ($batchGroup as $workerIdx => $batchRecipients) {
            if (empty($batchRecipients)) continue;
            
            $pid = pcntl_fork();
            
            if ($pid === -1) {
                // Fork failed - log error and continue with next
                error_log("Campaign $campaignId: Failed to fork worker process for worker $workerIdx");
                continue;
            } elseif ($pid === 0) {
                // Child process - process this batch
                process_worker_batch($dbConfig, $profile, $campaign, $batchRecipients, $isTest);
                // process_worker_batch calls exit(), so we never reach here
            } else {
                // Parent process - store child PID
                $batchPids[] = $pid;
                $allWorkerPids[] = $pid;
            }
        }
        
        // Add delay between batch groups (except after the last batch)
        // This allows earlier workers to establish connections and start working
        // before the next batch tries to connect, preventing connection spikes
        if ($batchGroupIdx < count($workerBatchGroups) - 1 && WORKER_SPAWN_BATCH_DELAY_MS > 0) {
            usleep(WORKER_SPAWN_BATCH_DELAY_MS * 1000); // Convert milliseconds to microseconds
            error_log("Campaign $campaignId (profile {$profile['id']}): Forked batch group " . ($batchGroupIdx + 1) . "/" . count($workerBatchGroups) . " (" . count($allWorkerPids) . " workers so far), waiting " . WORKER_SPAWN_BATCH_DELAY_MS . "ms before next batch");
        }
    }
    
    error_log("Campaign $campaignId (profile {$profile['id']}): All " . count($allWorkerPids) . " workers forked, waiting for completion");
    
    // Parent process: wait for all workers to complete
    wait_for_workers($allWorkerPids);
    $sent = 0;
    // Note: We can't easily get sent/failed counts from child processes
    // The database events table will have the accurate data
    
    // Count results from database for accurate totals
    try {
        $stmt = $pdo->prepare("
            SELECT COUNT(*) 
            FROM events 
            WHERE campaign_id = ? 
            AND event_type = 'delivered' 
            AND JSON_EXTRACT(details, '$.worker_pid') IS NOT NULL
            AND created_at >= DATE_SUB(NOW(), INTERVAL 10 MINUTE)
        ");
        $stmt->execute([$campaignId]);
        $sent = (int)$stmt->fetchColumn();
    } catch (Exception $e) {
        $sent = 0;
    }
    
    return ['sent' => $sent, 'failed' => $failed];
}

/**
 * Multi-profile sending function for rotation mode
 * 
 * When rotation is enabled, this function distributes recipients evenly across ALL active profiles.
 * Each profile then uses its own connection_numbers to further parallelize sending.
 * 
 * Example: 1000 emails, 4 active profiles → 250 emails per profile
 * If profile 1 has 10 connections → 25 emails per connection within profile 1
 * 
 * Status flow: draft → queued (orange) → sending (yellow) → sent (green)
 */
function send_campaign_with_rotation(PDO $pdo, array $campaign, array $recipients, array $activeProfiles, bool $isTest = false) {
    try {
        if (empty($recipients) || empty($activeProfiles)) {
            error_log("send_campaign_with_rotation called with empty recipients or profiles");
            return ['sent' => 0, 'failed' => count($recipients)];
        }
        
        $total = count($recipients);
        $campaignId = (int)$campaign['id'];
        
        error_log("Starting rotation send for campaign {$campaignId} with {$total} recipients and " . count($activeProfiles) . " profiles");
    
    // PHASE 1: QUEUED - Set status to queued and prepare batches across profiles
    if (!$isTest) {
        update_campaign_progress($pdo, $campaignId, 0, $total, 'queued');
        try {
            $stmt = $pdo->prepare("UPDATE campaigns SET status='queued', total_recipients=? WHERE id=?");
            $stmt->execute([$total, $campaignId]);
        } catch (Exception $e) {}
    }
    
    // Divide recipients evenly across all active profiles
    $profileCount = count($activeProfiles);
    $recipientsPerProfile = ceil($total / $profileCount);
    
    $profileBatches = [];
    for ($i = 0; $i < $profileCount; $i++) {
        $start = $i * $recipientsPerProfile;
        if ($start >= $total) break;
        
        $profileRecipients = array_slice($recipients, $start, $recipientsPerProfile);
        if (!empty($profileRecipients)) {
            $profileBatches[] = [
                'profile' => $activeProfiles[$i],
                'recipients' => $profileRecipients
            ];
        }
    }
    
    // PHASE 2: SENDING - Update status to sending
    if (!$isTest) {
        update_campaign_progress($pdo, $campaignId, 0, $total, 'sending');
        try {
            $stmt = $pdo->prepare("UPDATE campaigns SET status='sending', total_recipients=? WHERE id=?");
            $stmt->execute([$total, $campaignId]);
        } catch (Exception $e) {}
    }
    
    // Check if we can fork processes for concurrent profile sending
    $canFork = function_exists('pcntl_fork') && function_exists('pcntl_waitpid');
    
    $totalSent = 0;
    $totalFailed = 0;
    
    if ($canFork && !$isTest) {
        // CONCURRENT MODE: Fork a process for each profile
        global $DB_HOST, $DB_NAME, $DB_USER, $DB_PASS;
        
        $profilePids = [];
        
        foreach ($profileBatches as $batch) {
            $profile = $batch['profile'];
            $profileRecipients = $batch['recipients'];
            
            $pid = pcntl_fork();
            
            if ($pid === -1) {
                // Fork failed - fall back to sequential for this profile
                error_log("Failed to fork process for profile {$profile['id']}");
                $result = send_campaign_with_connections_for_profile_concurrent(
                    $pdo, 
                    $campaign, 
                    $profileRecipients, 
                    $profile, 
                    $isTest
                );
                $totalSent += $result['sent'];
                $totalFailed += $result['failed'];
            } elseif ($pid === 0) {
                // Child process - handle this profile
                // Create new PDO connection for child
                try {
                    $childPdo = new PDO(
                        "mysql:host={$DB_HOST};dbname={$DB_NAME};charset=utf8mb4",
                        $DB_USER,
                        $DB_PASS,
                        [
                            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                            PDO::ATTR_TIMEOUT => DB_OPERATION_TIMEOUT,
                            PDO::ATTR_PERSISTENT => false, // Non-persistent to ensure clean connections
                            PDO::MYSQL_ATTR_INIT_COMMAND => "SET SESSION wait_timeout=" . DB_SESSION_TIMEOUT . ", SESSION interactive_timeout=" . DB_SESSION_TIMEOUT
                        ]
                    );
                } catch (Exception $e) {
                    error_log("Child PDO error: " . $e->getMessage());
                    exit(1);
                }
                
                // Process this profile's recipients with concurrent connections
                try {
                    send_campaign_with_connections_for_profile_concurrent(
                        $childPdo, 
                        $campaign, 
                        $profileRecipients, 
                        $profile, 
                        $isTest
                    );
                } finally {
                    // Explicitly close database connection before child process exit
                    $childPdo = null;
                }
                
                exit(0);
            } else {
                // Parent process - store child PID
                $profilePids[] = $pid;
            }
        }
        
        // Wait for all profile workers to complete
        wait_for_workers($profilePids);
        
        // Count results from database
        try {
            $stmt = $pdo->prepare("SELECT COUNT(*) FROM events WHERE campaign_id = ? AND event_type = 'delivered'");
            $stmt->execute([$campaignId]);
            $totalSent = (int)$stmt->fetchColumn();
            
            $stmt = $pdo->prepare("SELECT COUNT(*) FROM events WHERE campaign_id = ? AND event_type IN ('bounce', 'deferred', 'skipped_unsubscribe')");
            $stmt->execute([$campaignId]);
            $totalFailed = (int)$stmt->fetchColumn();
            
            // Get the actual progress_sent value that workers have been updating
            $stmt = $pdo->prepare("SELECT progress_sent FROM campaigns WHERE id = ?");
            $stmt->execute([$campaignId]);
            $actualProgressSent = (int)$stmt->fetchColumn();
            
            // Use the actual progress value if it's higher (workers may have updated it)
            $totalProcessed = max($totalSent + $totalFailed, $actualProgressSent);
        } catch (Exception $e) {
            error_log("Error counting results: " . $e->getMessage());
            // Use whatever values we have, defaulting to 0 if undefined
            $totalProcessed = (isset($totalSent) ? $totalSent : 0) + (isset($totalFailed) ? $totalFailed : 0);
        }
    } else {
        // SEQUENTIAL MODE: Process each profile one by one (fallback or test mode)
        foreach ($profileBatches as $batch) {
            $profile = $batch['profile'];
            $profileRecipients = $batch['recipients'];
            
            // Use concurrent connections within each profile
            $result = send_campaign_with_connections_for_profile_concurrent(
                $pdo, 
                $campaign, 
                $profileRecipients, 
                $profile, 
                $isTest
            );
            
            $totalSent += $result['sent'];
            $totalFailed += $result['failed'];
        }
    }
    
    // Final status update - CRITICAL: This must always execute
    // Wrapped in try-finally to ensure execution even if errors occur
    try {
        if (!$isTest) {
            // For concurrent mode, use the actual total processed from workers
            $progressToUpdate = isset($totalProcessed) ? $totalProcessed : ($totalSent + $totalFailed);
            
            // Mark as completed in progress tracker
            update_campaign_progress($pdo, $campaignId, $progressToUpdate, $total, 'completed');
            
            // Update main campaign status to 'sent'
            try {
                $stmt = $pdo->prepare("UPDATE campaigns SET status='sent', sent_at = COALESCE(sent_at, NOW()) WHERE id=?");
                $result = $stmt->execute([$campaignId]);
                if (!$result) {
                    error_log("Failed to update campaign status to sent for campaign ID: {$campaignId}");
                    // Force update as fallback (using prepared statement)
                    $fallbackStmt = $pdo->prepare("UPDATE campaigns SET status='sent', sent_at = COALESCE(sent_at, NOW()) WHERE id=?");
                    $fallbackStmt->execute([$campaignId]);
                } else {
                    error_log("Successfully updated campaign {$campaignId} status to sent (rotation mode)");
                }
            } catch (Exception $e) {
                error_log("Exception updating campaign status to sent (rotation): " . $e->getMessage());
                // Force update as last resort (using prepared statement)
                try {
                    $fallbackStmt = $pdo->prepare("UPDATE campaigns SET status='sent', sent_at = COALESCE(sent_at, NOW()) WHERE id=?");
                    $fallbackStmt->execute([$campaignId]);
                    error_log("Fallback status update successful for campaign {$campaignId}");
                } catch (Exception $e2) {
                    error_log("Fallback status update also failed: " . $e2->getMessage());
                }
            }
        }
    } finally {
        // Ensure we always log completion regardless of any errors
        error_log("Campaign {$campaignId} rotation send completed: sent={$totalSent}, failed={$totalFailed}, total={$total}");
    }
    
    return ['sent' => $totalSent, 'failed' => $totalFailed];
    
    } catch (Throwable $e) {
        error_log("CRITICAL ERROR in send_campaign_with_rotation: " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine());
        error_log("Stack trace: " . $e->getTraceAsString());
        return ['sent' => 0, 'failed' => isset($total) ? $total : 0];
    }
}

/**
 * Helper function for sending with a specific profile (used by multi-profile rotation)
 * Does NOT update campaign status - that's handled by the caller
 */
function send_campaign_with_connections_for_profile(PDO $pdo, array $campaign, array $recipients, array $profile, bool $isTest = false) {
    if (empty($recipients)) {
        return ['sent' => 0, 'failed' => 0];
    }
    
    $campaignId = (int)$campaign['id'];
    
    // Count recipients for auto-calculation
    $totalRecipients = count($recipients);
    
    // Auto-calculate workers based on email count: 5 workers per 1,000 emails (capped at MAX_WORKERS)
    // If profile has custom workers setting, use that as minimum
    $autoWorkers = calculate_workers_from_count($totalRecipients);
    // Enforce MAX_WORKERS cap on profile settings for resource safety
    $profileWorkers = max(MIN_WORKERS, min(MAX_WORKERS, (int)($profile['workers'] ?? DEFAULT_WORKERS)));
    
    // Use the higher of auto-calculated or profile-configured workers (both already capped)
    $workers = max($autoWorkers, $profileWorkers);
    
    // Log info about worker calculation for large campaigns only (>= 5000 emails)
    if ($totalRecipients >= 5000) {
        error_log("Campaign $campaignId (non-concurrent): $totalRecipients emails → auto-calculated $autoWorkers workers (profile setting: $profileWorkers, using: $workers, capped at MAX_WORKERS=" . MAX_WORKERS . ")");
    }
    
    // Log info for large worker counts (informational only)
    if ($workers >= WORKERS_WARNING_THRESHOLD) {
        error_log("Info: Profile {$profile['id']} using $workers workers for campaign $campaignId (non-concurrent mode, high throughput with resource safety cap)");
    }
    
    $ins = $pdo->prepare("INSERT INTO events (campaign_id, event_type, details) VALUES (?,?,?)");
    
    $sent = 0;
    $failed = 0;
    // Use a hash map for O(1) duplicate detection; the old array + in_array was O(N²).
    $attempted = [];
    
    // Filter out unsubscribed and prepare recipients
    $validRecipients = [];
    foreach ($recipients as $email) {
        $emailLower = strtolower(trim($email));
        if ($emailLower === '' || isset($attempted[$emailLower])) {
            continue;
        }
        $attempted[$emailLower] = true;
        
        // Check unsubscribed
        if (is_unsubscribed($pdo, $emailLower)) {
            $ins->execute([
                $campaignId,
                'skipped_unsubscribe',
                json_encode([
                    'rcpt' => $emailLower,
                    'reason' => 'unsubscribed',
                    'test' => $isTest ? 1 : 0,
                ])
            ]);
            $failed++;
            continue;
        }
        
        $validRecipients[] = $emailLower;
    }
    unset($attempted);
    
    // Process all valid recipients with this profile
    // (No batching needed - send all emails directly)
    
    // Prepare campaign for sending
    $campaignSend = $campaign;
    $campaignSend['from_email'] = trim($profile['from_email'] ?? $campaign['from_email']);
    $campaignSend['sender_name'] = trim($profile['sender_name'] ?? $campaign['sender_name']);
    
    // For API profiles, skip pre-building the HTML map — api_send_batch builds HTML
    // lazily per batch (one build per 500 recipients instead of one per recipient).
    // This prevents memory exhaustion on large lists (e.g. 100k+ recipients).
    // For SMTP profiles, build per-recipient HTML (unique tracking URLs per email).
    if (($profile['type'] ?? '') === 'api') {
        $htmlMap = [];
    } else {
        $htmlMap = [];
        foreach ($validRecipients as $emailLower) {
            $htmlMap[$emailLower] = build_tracked_html($campaignSend, $emailLower);
        }
    }
    
    try {
        // Send all recipients
        if (isset($profile['type']) && $profile['type'] === 'api') {
            // API batch sending – groups recipients into batches of API_RECIPIENTS_PER_REQUEST,
            // delivered via curl_multi pool with HTTP/2 and persistent connections.
            // $onBatchComplete fires per curl_multi batch (before $onProgress) to insert event
            // rows immediately, keeping 'delivered' count in sync with progress_sent.
            $apiProgressStmt = null;
            api_send_batch($profile, $campaignSend, $validRecipients, $htmlMap,
                // $onProgress: update progress_sent after events are already inserted
                !$isTest ? function(int $delta) use ($campaignId, $pdo, &$apiProgressStmt) {
                    try {
                        if (!isset($apiProgressStmt)) {
                            $apiProgressStmt = $pdo->prepare("UPDATE campaigns SET progress_sent = progress_sent + ? WHERE id = ?");
                        }
                        $apiProgressStmt->execute([$delta, $campaignId]);
                    } catch (Exception $e) {}
                } : null,
                // $onBatchComplete: insert event rows immediately so delivered updates during send
                function(array $batchEmails, array $batchResults) use (
                    $campaignId, $profile, $isTest, $ins, $pdo, &$sent, &$failed
                ) {
                    foreach ($batchEmails as $emailLower) {
                        $res = $batchResults[$emailLower] ?? [
                            'ok' => false, 'type' => 'deferred', 'code' => null, 'msg' => 'No batch result',
                        ];
                        if (!empty($res['ok']) && ($res['type'] ?? '') === 'delivered') {
                            $ins->execute([
                                $campaignId,
                                'delivered',
                                json_encode([
                                    'rcpt' => $emailLower,
                                    'profile_id' => $profile['id'] ?? null,
                                    'via' => $profile['type'] ?? null,
                                    'smtp_code' => $res['code'] ?? null,
                                    'test' => $isTest ? 1 : 0,
                                ])
                            ]);
                            $sent++;
                        } else {
                            $etype = ($res['type'] === 'deferred') ? 'deferred' : 'bounce';
                            $ins->execute([
                                $campaignId,
                                $etype,
                                json_encode([
                                    'rcpt' => $emailLower,
                                    'profile_id' => $profile['id'] ?? null,
                                    'via' => $profile['type'] ?? null,
                                    'smtp_code' => $res['code'] ?? null,
                                    'smtp_msg' => $res['msg'] ?? '',
                                    'test' => $isTest ? 1 : 0,
                                ])
                            ]);
                            if ($etype === 'bounce') {
                                add_to_unsubscribes($pdo, $emailLower);
                            }
                            $failed++;
                        }
                    }
                });
        } else {
            // SMTP batch sending with persistent connection
            $batchResult = smtp_send_batch($profile, $campaignSend, $validRecipients, $htmlMap);
            
            if (isset($batchResult['results']) && is_array($batchResult['results'])) {
                foreach ($batchResult['results'] as $recipientEmail => $res) {
                    if (!empty($res['ok']) && ($res['type'] ?? '') === 'delivered') {
                        $ins->execute([
                            $campaignId,
                            'delivered',
                            json_encode([
                                'rcpt' => $recipientEmail,
                                'profile_id' => $profile['id'] ?? null,
                                'via' => 'smtp_batch',
                                'smtp_code' => $res['code'] ?? null,
                                'test' => $isTest ? 1 : 0,
                            ])
                        ]);
                        $sent++;
                    } else {
                        $etype = ($res['type'] === 'deferred') ? 'deferred' : 'bounce';
                        $ins->execute([
                            $campaignId,
                            $etype,
                            json_encode([
                                'rcpt' => $recipientEmail,
                                'profile_id' => $profile['id'] ?? null,
                                'via' => 'smtp_batch',
                                'smtp_code' => $res['code'] ?? null,
                                'smtp_msg' => $res['msg'] ?? '',
                                'test' => $isTest ? 1 : 0,
                            ])
                        ]);
                        
                        if ($etype === 'bounce') {
                            add_to_unsubscribes($pdo, $recipientEmail);
                        }
                        $failed++;
                    }
                }
            } else {
                // Batch failed entirely (connection error)
                foreach ($validRecipients as $emailLower) {
                    $ins->execute([
                        $campaignId,
                        'bounce',
                        json_encode([
                            'rcpt' => $emailLower,
                            'error' => $batchResult['error'] ?? 'Connection failed',
                            'profile_id' => $profile['id'] ?? null,
                            'via' => 'smtp_batch',
                            'test' => $isTest ? 1 : 0,
                        ])
                    ]);
                    add_to_unsubscribes($pdo, $emailLower);
                    $failed++;
                }
            }
        }
    } catch (Exception $e) {
        // Handle exception
        foreach ($validRecipients as $emailLower) {
            $ins->execute([
                $campaignId,
                'bounce',
                json_encode([
                    'rcpt' => $emailLower,
                    'error' => 'Exception: ' . $e->getMessage(),
                    'profile_id' => $profile['id'] ?? null,
                    'test' => $isTest ? 1 : 0,
                ])
            ]);
            add_to_unsubscribes($pdo, $emailLower);
            $failed++;
        }
    }
    
    return ['sent' => $sent, 'failed' => $failed];
}

/**
 * Optimized sending function using connection-based batching with persistent connections
 * 
 * Implements a two-phase process:
 * 1. QUEUED phase: Divides recipients evenly across connection_numbers (e.g., 40 connections = 40 batches)
 * 2. SENDING phase: Sends emails using persistent SMTP connections, reconnecting every batch_size emails
 * 
 * Status flow: draft → queued (orange) → sending (yellow) → sent (green)
 */
function send_campaign_with_connections(PDO $pdo, array $campaign, array $recipients, ?int $profileId = null, bool $isTest = false) {
    if (empty($recipients)) {
        return ['sent' => 0, 'failed' => 0];
    }
    
    $total = count($recipients);
    $campaignId = (int)$campaign['id'];
    
    // Get profile first to determine connection numbers
    $profile = null;
    if ($profileId) {
        $stmt = $pdo->prepare("SELECT * FROM sending_profiles WHERE id = ? LIMIT 1");
        $stmt->execute([$profileId]);
        $profile = $stmt->fetch(PDO::FETCH_ASSOC);
    } else {
        $profile = find_profile_for_campaign($pdo, $campaign);
    }
    
    if (!$profile) {
        return ['sent' => 0, 'failed' => $total, 'error' => 'No active profile'];
    }
    
    // PHASE 1: QUEUED - Set status to queued and prepare batches
    if (!$isTest) {
        update_campaign_progress($pdo, $campaignId, 0, $total, 'queued');
        try {
            $stmt = $pdo->prepare("UPDATE campaigns SET status='queued', total_recipients=? WHERE id=?");
            $stmt->execute([$total, $campaignId]);
        } catch (Exception $e) {}
    }
    
    // Read messages_per_worker FIRST — this drives the worker count calculation.
    // Cap at DEFAULT_MESSAGES_PER_WORKER so a stale/large DB value cannot shrink the
    // number of workers that actually get forked (old DB rows may still hold 1000 or 2000).
    $messagesPerWorker = resolve_messages_per_worker((int)($profile['messages_per_worker'] ?? DEFAULT_MESSAGES_PER_WORKER));
    
    // Compute worker count from the SAME divisor used in the distribution loop below.
    // Using calculate_workers_from_count() (which divides by DEFAULT_MESSAGES_PER_WORKER)
    // while then distributing with a DIFFERENT chunk size is the hidden bug that causes
    // "117 slots allocated, only 59 filled".
    $autoWorkers = max(MIN_WORKERS, min(MAX_WORKERS, intdiv($total, $messagesPerWorker)));
    // Enforce MAX_WORKERS cap on profile settings for resource safety
    $profileWorkers = max(MIN_WORKERS, min(MAX_WORKERS, (int)($profile['workers'] ?? DEFAULT_WORKERS)));
    
    // Use the higher of auto-calculated or profile-configured workers (both already capped)
    $workers = max($autoWorkers, $profileWorkers);
    
    // Log info about worker calculation for large campaigns only (>= 5000 emails)
    if ($total >= 5000) {
        error_log("Campaign $campaignId: $total emails → $autoWorkers workers × $messagesPerWorker emails/worker (profile setting: $profileWorkers, using: $workers)");
    }
    
    // Log info for large worker counts (informational only)
    if ($workers >= WORKERS_WARNING_THRESHOLD) {
        error_log("Info: Profile {$profile['id']} using $workers workers for campaign $campaignId (high throughput mode with resource safety cap)");
    }
    
    $ins = $pdo->prepare("INSERT INTO events (campaign_id, event_type, details) VALUES (?,?,?)");
    
    $sent = 0;
    $failed = 0;
    // Use a hash map for O(1) duplicate detection; the old array + in_array was O(N²)
    // and blocked the parent process for minutes on large lists (e.g. 90k+ recipients).
    $attempted = [];
    
    // Filter out unsubscribed and prepare recipients
    $validRecipients = [];
    foreach ($recipients as $email) {
        $emailLower = strtolower(trim($email));
        if ($emailLower === '' || isset($attempted[$emailLower])) {
            continue;
        }
        $attempted[$emailLower] = true;
        
        // Check unsubscribed
        if (is_unsubscribed($pdo, $emailLower)) {
            $ins->execute([
                $campaignId,
                'skipped_unsubscribe',
                json_encode([
                    'rcpt' => $emailLower,
                    'reason' => 'unsubscribed',
                    'test' => $isTest ? 1 : 0,
                ])
            ]);
            $failed++;
            continue;
        }
        
        $validRecipients[] = $emailLower;
    }
    unset($attempted);
    $totalValidRecipients = count($validRecipients);
    
    // Spawn up to $workers parallel processes (but not more than total recipients)
    $actualWorkers = min($workers, $totalValidRecipients);
    
    // Distribute recipients using round-robin with messagesPerWorker as batch size
    // This matches the spawn_parallel_workers logic
    $workerRecipients = array_fill(0, $actualWorkers, []);
    $cycleDelayMs = (int)($profile['cycle_delay_ms'] ?? DEFAULT_CYCLE_DELAY_MS);
    
    $recipientIdx = 0;
    $roundCount = 0;
    while ($recipientIdx < $totalValidRecipients) {
        // No cycle delay during queued phase - optimize for speed
        
        for ($workerIdx = 0; $workerIdx < $actualWorkers && $recipientIdx < $totalValidRecipients; $workerIdx++) {
            $chunkSize = min($messagesPerWorker, $totalValidRecipients - $recipientIdx);
            $chunk = array_slice($validRecipients, $recipientIdx, $chunkSize);
            // Use array_push with spread operator for O(n) performance instead of array_merge O(n²)
            array_push($workerRecipients[$workerIdx], ...$chunk);
            $recipientIdx += $chunkSize;
        }
        
        $roundCount++;
    }
    
    // Convert to worker batches array (filter out empty)
    $workerBatches = [];
    foreach ($workerRecipients as $recips) {
        if (!empty($recips)) {
            $workerBatches[] = $recips;
        }
    }
    
    // PHASE 2: SENDING - Update status to sending when we start actual sending
    if (!$isTest) {
        update_campaign_progress($pdo, $campaignId, 0, $total, 'sending');
        try {
            $stmt = $pdo->prepare("UPDATE campaigns SET status='sending', total_recipients=? WHERE id=?");
            $stmt->execute([$total, $campaignId]);
        } catch (Exception $e) {}
    }
    
    // Prepare campaign for sending
    $campaignSend = $campaign;
    $campaignSend['from_email'] = trim($profile['from_email'] ?? $campaign['from_email']);
    $campaignSend['sender_name'] = trim($profile['sender_name'] ?? $campaign['sender_name']);
    
    // Check if we can use concurrent processing
    $canFork = function_exists('pcntl_fork') && function_exists('pcntl_waitpid');
    
    // API profiles MUST NOT use the worker-fork mechanism. api_send_batch already handles
    // parallelism via an 8-handle curl_multi pool with built-in rate-limiting. Forking N
    // workers for an API profile fires N simultaneous API requests (one per worker), with
    // no inter-request pacing — this overwhelms the API (429s) and exhausts MySQL
    // max_connections (workers exit(1) silently losing all their recipients).
    // The sequential path calls api_send_batch once with all recipients, which is correct.
    if ($canFork && !$isTest && count($workerBatches) > 1 && ($profile['type'] ?? '') !== 'api') {
        // CONCURRENT MODE: Fork a worker for each batch
        global $DB_HOST, $DB_NAME, $DB_USER, $DB_PASS;
        
        $dbConfig = [
            'host' => $DB_HOST,
            'name' => $DB_NAME,
            'user' => $DB_USER,
            'pass' => $DB_PASS,
        ];
        
        // OPTIMIZATION: Spawn workers in batches to prevent resource exhaustion
        // Instead of forking all workers at once (which can be 500+ for large campaigns),
        // fork them in batches of MAX_CONCURRENT_WORKERS with delays between batches
        $totalWorkers = count($workerBatches);
        $batchSize = MAX_CONCURRENT_WORKERS;
        $workerBatchGroups = array_chunk($workerBatches, $batchSize, true);
        
        error_log("Campaign $campaignId: Forking $totalWorkers workers in " . count($workerBatchGroups) . " batch groups of up to $batchSize workers each");
        
        $allWorkerPids = [];
        
        foreach ($workerBatchGroups as $batchGroupIdx => $batchGroup) {
            $batchPids = [];
            
            foreach ($batchGroup as $workerIdx => $workerRecipients) {
                $pid = pcntl_fork();
                
                if ($pid === -1) {
                    // Fork failed - log and continue with sequential fallback
                    error_log("Campaign $campaignId: Failed to fork worker #$workerIdx");
                    continue;
                } elseif ($pid === 0) {
                    // Child process - send this batch
                    process_worker_batch($dbConfig, $profile, $campaignSend, $workerRecipients, $isTest);
                    // process_worker_batch calls exit(0) on success; add an explicit exit
                    // here as a safety net so the child never falls through to parent code
                    // if process_worker_batch returns unexpectedly (e.g. after an exception).
                    exit(0);
                } else {
                    // Parent process
                    $batchPids[] = $pid;
                    $allWorkerPids[] = $pid;
                }
            }
            
            // Add delay between batch groups (except after the last batch)
            // This allows earlier workers to establish connections and start working
            // before the next batch tries to connect, preventing connection spikes
            if ($batchGroupIdx < count($workerBatchGroups) - 1 && WORKER_SPAWN_BATCH_DELAY_MS > 0) {
                usleep(WORKER_SPAWN_BATCH_DELAY_MS * 1000); // Convert milliseconds to microseconds
                error_log("Campaign $campaignId: Forked batch group " . ($batchGroupIdx + 1) . "/" . count($workerBatchGroups) . " (" . count($allWorkerPids) . " workers so far), waiting " . WORKER_SPAWN_BATCH_DELAY_MS . "ms before next batch");
            }
        }
        
        error_log("Campaign $campaignId: All " . count($allWorkerPids) . " workers forked, waiting for completion");
        
        // Wait for all workers
        wait_for_workers($allWorkerPids);
        
        // Count results from database (both sent and failed)
        try {
            $stmt = $pdo->prepare("SELECT COUNT(*) FROM events WHERE campaign_id = ? AND event_type = 'delivered'");
            $stmt->execute([$campaignId]);
            $sent = (int)$stmt->fetchColumn();
            
            $stmt = $pdo->prepare("SELECT COUNT(*) FROM events WHERE campaign_id = ? AND event_type IN ('bounce', 'deferred', 'skipped_unsubscribe')");
            $stmt->execute([$campaignId]);
            $failed = (int)$stmt->fetchColumn();
            
            // Get the actual progress_sent value that workers have been updating
            $stmt = $pdo->prepare("SELECT progress_sent FROM campaigns WHERE id = ?");
            $stmt->execute([$campaignId]);
            $actualProgressSent = (int)$stmt->fetchColumn();
            
            // Use the actual progress value if it's higher (workers may have updated it)
            $totalProcessed = max($sent + $failed, $actualProgressSent);
        } catch (Exception $e) {
            $sent = 0;
            $failed = 0;
            $totalProcessed = 0;
        }
    } else {
        // SEQUENTIAL MODE: Process all recipients sequentially (fallback or test mode)
        // For API profiles, skip pre-building the HTML map — api_send_batch builds HTML
        // lazily per batch (one build per 500 recipients instead of one per recipient).
        // For SMTP profiles, build per-recipient HTML (unique tracking URLs per email).
        if (($profile['type'] ?? '') === 'api') {
            $htmlMap = [];
        } else {
            $htmlMap = [];
            foreach ($validRecipients as $emailLower) {
                $htmlMap[$emailLower] = build_tracked_html($campaignSend, $emailLower);
            }
        }
        
        try {
            // Send all recipients
            if (isset($profile['type']) && $profile['type'] === 'api') {
                // API batch sending – groups recipients into batches of API_RECIPIENTS_PER_REQUEST,
                // delivered via curl_multi pool with HTTP/2 and persistent connections.
                // $onBatchComplete fires per curl_multi batch (before $onProgress) to insert event
                // rows immediately, keeping 'delivered' count in sync with progress_sent.
                $seqApiProgressStmt = null;
                api_send_batch($profile, $campaignSend, $validRecipients, $htmlMap,
                    // $onProgress: update progress_sent after events are already inserted
                    !$isTest ? function(int $delta) use ($campaignId, $pdo, &$seqApiProgressStmt) {
                        try {
                            if (!isset($seqApiProgressStmt)) {
                                $seqApiProgressStmt = $pdo->prepare("UPDATE campaigns SET progress_sent = progress_sent + ? WHERE id = ?");
                            }
                            $seqApiProgressStmt->execute([$delta, $campaignId]);
                        } catch (Exception $e) {}
                    } : null,
                    // $onBatchComplete: insert event rows immediately so delivered updates during send
                    function(array $batchEmails, array $batchResults) use (
                        $campaignId, $profile, $isTest, $ins, $pdo, &$sent, &$failed
                    ) {
                        foreach ($batchEmails as $emailLower) {
                            $res = $batchResults[$emailLower] ?? [
                                'ok' => false, 'type' => 'deferred', 'code' => null, 'msg' => 'No batch result',
                            ];
                            if (!empty($res['ok']) && ($res['type'] ?? '') === 'delivered') {
                                $ins->execute([
                                    $campaignId,
                                    'delivered',
                                    json_encode([
                                        'rcpt' => $emailLower,
                                        'profile_id' => $profile['id'] ?? null,
                                        'via' => $profile['type'] ?? null,
                                        'smtp_code' => $res['code'] ?? null,
                                        'test' => $isTest ? 1 : 0,
                                    ])
                                ]);
                                $sent++;
                            } else {
                                $etype = ($res['type'] === 'deferred') ? 'deferred' : 'bounce';
                                $ins->execute([
                                    $campaignId,
                                    $etype,
                                    json_encode([
                                        'rcpt' => $emailLower,
                                        'profile_id' => $profile['id'] ?? null,
                                        'via' => $profile['type'] ?? null,
                                        'smtp_code' => $res['code'] ?? null,
                                        'smtp_msg' => $res['msg'] ?? '',
                                        'test' => $isTest ? 1 : 0,
                                    ])
                                ]);
                                if ($etype === 'bounce') {
                                    add_to_unsubscribes($pdo, $emailLower);
                                }
                                $failed++;
                            }
                        }
                    });
            } else {
                // SMTP batch sending with persistent connection
                $batchResult = smtp_send_batch($profile, $campaignSend, $validRecipients, $htmlMap);
                
                if (isset($batchResult['results']) && is_array($batchResult['results'])) {
                    foreach ($batchResult['results'] as $recipientEmail => $res) {
                        if (!empty($res['ok']) && ($res['type'] ?? '') === 'delivered') {
                            $ins->execute([
                                $campaignId,
                                'delivered',
                                json_encode([
                                    'rcpt' => $recipientEmail,
                                    'profile_id' => $profile['id'] ?? null,
                                    'via' => 'smtp_batch',
                                    'smtp_code' => $res['code'] ?? null,
                                    'test' => $isTest ? 1 : 0,
                                ])
                            ]);
                            $sent++;
                        } else {
                            $etype = ($res['type'] === 'deferred') ? 'deferred' : 'bounce';
                            $ins->execute([
                                $campaignId,
                                $etype,
                                json_encode([
                                    'rcpt' => $recipientEmail,
                                    'profile_id' => $profile['id'] ?? null,
                                    'via' => 'smtp_batch',
                                    'smtp_code' => $res['code'] ?? null,
                                    'smtp_msg' => $res['msg'] ?? '',
                                    'test' => $isTest ? 1 : 0,
                                ])
                            ]);
                            
                            if ($etype === 'bounce') {
                                add_to_unsubscribes($pdo, $recipientEmail);
                            }
                            $failed++;
                        }
                    }
                } else {
                    // Batch failed entirely (connection error)
                    foreach ($validRecipients as $emailLower) {
                        $ins->execute([
                            $campaignId,
                            'bounce',
                            json_encode([
                                'rcpt' => $emailLower,
                                'error' => $batchResult['error'] ?? 'Connection failed',
                                'profile_id' => $profile['id'] ?? null,
                                'via' => 'smtp_batch',
                                'test' => $isTest ? 1 : 0,
                            ])
                        ]);
                        add_to_unsubscribes($pdo, $emailLower);
                        $failed++;
                    }
                }
            }
        } catch (Exception $e) {
            // Handle exception
            foreach ($validRecipients as $emailLower) {
                $ins->execute([
                    $campaignId,
                    'bounce',
                    json_encode([
                        'rcpt' => $emailLower,
                        'error' => 'Exception: ' . $e->getMessage(),
                        'profile_id' => $profile['id'] ?? null,
                        'test' => $isTest ? 1 : 0,
                    ])
                ]);
                add_to_unsubscribes($pdo, $emailLower);
                $failed++;
            }
        }
        
        // Update progress periodically
        if (!$isTest && ($sent + $failed) % PROGRESS_UPDATE_FREQUENCY === 0) {
            update_campaign_progress($pdo, $campaignId, $sent + $failed, $total, 'sending');
        }
    } // End of if-else concurrent/sequential
    
    // Final status update - CRITICAL: This must always execute
    // Wrapped in try-finally to ensure execution even if errors occur
    try {
        if (!$isTest) {
            // For concurrent mode, use the actual total processed from workers
            $progressToUpdate = isset($totalProcessed) ? $totalProcessed : ($sent + $failed);
            
            // Mark as completed in progress tracker
            update_campaign_progress($pdo, $campaignId, $progressToUpdate, $total, 'completed');
            
            // Update main campaign status to 'sent'
            try {
                $stmt = $pdo->prepare("UPDATE campaigns SET status='sent', sent_at = COALESCE(sent_at, NOW()) WHERE id=?");
                $result = $stmt->execute([$campaignId]);
                if (!$result) {
                    error_log("Failed to update campaign status to sent for campaign ID: {$campaignId}");
                    // Force update as fallback (using prepared statement)
                    $fallbackStmt = $pdo->prepare("UPDATE campaigns SET status='sent', sent_at = COALESCE(sent_at, NOW()) WHERE id=?");
                    $fallbackStmt->execute([$campaignId]);
                } else {
                    error_log("Successfully updated campaign {$campaignId} status to sent");
                }
            } catch (Exception $e) {
                error_log("Exception updating campaign status to sent: " . $e->getMessage());
                // Force update as last resort (using prepared statement)
                try {
                    $fallbackStmt = $pdo->prepare("UPDATE campaigns SET status='sent', sent_at = COALESCE(sent_at, NOW()) WHERE id=?");
                    $fallbackStmt->execute([$campaignId]);
                    error_log("Fallback status update successful for campaign {$campaignId}");
                } catch (Exception $e2) {
                    error_log("Fallback status update also failed: " . $e2->getMessage());
                }
            }
        }
    } finally {
        // Ensure we always log completion regardless of any errors
        error_log("Campaign {$campaignId} send process completed: sent={$sent}, failed={$failed}, total={$total}");
    }
    
    return ['sent' => $sent, 'failed' => $failed];
}

/**
 * IMAP bounce processing — unchanged
 */
function process_imap_bounces(PDO $pdo) {
    if (!function_exists('imap_open')) {
        return;
    }

    // CRITICAL: Acquire fetch lock to prevent running during send operations
    // This ensures separation of fetch and send as per requirements
    $fetchLock = acquire_operation_lock('fetch', FETCH_LOCK_TIMEOUT);
    if ($fetchLock === false) {
        error_log("Bounce scan aborted: Could not acquire fetch lock (send operation may be running)");
        return;
    }
    
    try {
        // NOTE: The fetch lock is sufficient for mutual exclusion
        // No need for additional checks - if we hold fetch lock, send cannot start
        // If send is running when we try to acquire fetch lock, we'll timeout above

        $profiles = get_profiles($pdo);
        $ins = $pdo->prepare("INSERT INTO events (campaign_id, event_type, details) VALUES (?,?,?)");

        foreach ($profiles as $p) {
            $server = trim($p['bounce_imap_server'] ?? '');
            $user   = trim($p['bounce_imap_user'] ?? '');
            $pass   = trim($p['bounce_imap_pass'] ?? '');

            if ($server === '' || $user === '' || $pass === '') continue;

            $mailbox = $server;
            if (stripos($mailbox, '{') !== 0) {
                $hostOnly = $mailbox;
                $mailbox = "{" . $hostOnly . ":993/imap/ssl}INBOX";
            }

            try {
                $mbox = @imap_open($mailbox, $user, $pass, 0, 1);
                if (!$mbox) {
                    @imap_close($mbox);
                    continue;
                }

                $msgs = @imap_search($mbox, 'UNSEEN');
                if ($msgs === false) {
                    $msgs = @imap_search($mbox, 'ALL');
                    if ($msgs === false) $msgs = [];
                }

                foreach ($msgs as $msgno) {
                    $header = @imap_headerinfo($mbox, $msgno);
                    $body = @imap_body($mbox, $msgno);

                    $foundEmails = [];

                    if ($body) {
                        if (preg_match_all('/Final-Recipient:\s*.*?;\s*([^\s;<>"]+)/i', $body, $m1)) {
                            foreach ($m1[1] as $e) $foundEmails[] = $e;
                        }
                        if (preg_match_all('/Original-Recipient:\s*.*?;\s*([^\s;<>"]+)/i', $body, $m2)) {
                            foreach ($m2[1] as $e) $foundEmails[] = $e;
                        }
                    }

                    $hdrText = imap_fetchheader($mbox, $msgno);
                    if ($hdrText) {
                        if (preg_match_all('/(?:To|Delivered-To|X-Original-To):\s*(.*)/i', $hdrText, $mt)) {
                            foreach ($mt[1] as $chunk) {
                                if (preg_match_all('/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i', $chunk, $emails)) {
                                    foreach ($emails[0] as $e) $foundEmails[] = $e;
                                }
                            }
                        }
                    }

                    if ($body) {
                        if (preg_match_all('/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i', $body, $eb)) {
                            foreach ($eb[0] as $e) $foundEmails[] = $e;
                        }
                    }

                    $foundEmails = array_unique(array_map('strtolower', array_filter(array_map('trim', $foundEmails))));
                    if (!empty($foundEmails)) {
                        foreach ($foundEmails as $rcpt) {
                            try {
                                $details = [
                                    'rcpt' => $rcpt,
                                    'profile_id' => $p['id'],
                                    'source' => 'imap_bounce',
                                    'subject' => isset($header->subject) ? (string)$header->subject : '',
                                ];
                                $ins->execute([0, 'bounce', json_encode($details)]);
                            } catch (Exception $e) {}

                            try {
                                add_to_unsubscribes($pdo, $rcpt);
                            } catch (Exception $e) {}
                        }
                    }

                    @imap_setflag_full($mbox, $msgno, "\\Seen");
                }

                @imap_close($mbox);
            } catch (Exception $e) {}
        }
    } finally {
        // CRITICAL: Always release the fetch lock, even if an error occurs
        release_operation_lock($fetchLock, 'fetch');
        error_log("Bounce scan completed and fetch lock released");
    }
}

///////////////////////
//  API ENDPOINT FOR REAL-TIME CAMPAIGN PROGRESS
///////////////////////
if (isset($_GET['api']) && $_GET['api'] === 'progress' && isset($_GET['campaign_id'])) {
    header('Content-Type: application/json');
    $campaignId = (int)$_GET['campaign_id'];
    $progress = get_campaign_progress($pdo, $campaignId);
    $stats = get_campaign_stats($pdo, $campaignId);
    
    echo json_encode([
        'success' => true,
        'progress' => $progress,
        'stats' => [
            'delivered' => $stats['delivered'],
            'bounce' => $stats['bounce'],
            'open' => $stats['open'],
            'click' => $stats['click'],
            'unsubscribe' => $stats['unsubscribe']
        ]
    ]);
    // Close database connection before exit
    $pdo = null;
    exit;
}

///////////////////////
//  TRACKING ENDPOINTS (open, click, unsubscribe)
///////////////////////
if (isset($_GET['t']) && in_array($_GET['t'], ['open','click','unsubscribe'], true)) {
    $t   = $_GET['t'];
    $cid = isset($_GET['cid']) ? (int)$_GET['cid'] : 0;

    if ($cid > 0) {
        $rcpt = '';
        if (!empty($_GET['r'])) {
            $rcptDecoded = base64url_decode($_GET['r']);
            if (is_string($rcptDecoded)) {
                $rcpt = $rcptDecoded;
            }
        }

        $details = [
            'rcpt' => $rcpt,
            'ip'   => $_SERVER['REMOTE_ADDR'] ?? '',
            'ua'   => $_SERVER['HTTP_USER_AGENT'] ?? '',
        ];
        
        // Get device type from User Agent
        $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
        if ($userAgent !== '') {
            $details['device'] = get_device_type($userAgent);
        }
        
        // Get location from IP address
        $ip = $_SERVER['REMOTE_ADDR'] ?? '';
        if ($ip !== '') {
            $location = get_location_from_ip($ip);
            if ($location['country'] !== '' || $location['state'] !== '') {
                $details['country'] = $location['country'];
                $details['state'] = $location['state'];
            }
        }

        $url = '';
        if ($t === 'click') {
            $uParam = $_GET['u'] ?? '';
            if ($uParam !== '') {
                $decoded = base64url_decode($uParam);
                if (is_string($decoded)) {
                    $url = $decoded;
                }
            }
            $details['url'] = $url;
        }

        $eventType = $t === 'open' ? 'open' : ($t === 'click' ? 'click' : 'unsubscribe');

        try {
            $stmt = $pdo->prepare("INSERT INTO events (campaign_id, event_type, details) VALUES (?,?,?)");
            $stmt->execute([$cid, $eventType, json_encode($details)]);
        } catch (Exception $e) {}

        if ($t === 'unsubscribe' && $rcpt !== '') {
            try {
                add_to_unsubscribes($pdo, $rcpt);
            } catch (Exception $e) { /* ignore */ }
        }

        // Special enhancement: if this is a click and we don't have an 'open' recorded for this rcpt+campaign,
        // create an estimated open event. This helps when clients block images (Yahoo/AOL etc.).
        if ($t === 'click' && $rcpt !== '') {
            try {
                if (!has_open_event_for_rcpt($pdo, $cid, $rcpt)) {
                    $estimatedOpen = [
                        'rcpt' => $rcpt,
                        'ip'   => $_SERVER['REMOTE_ADDR'] ?? '',
                        'ua'   => $_SERVER['HTTP_USER_AGENT'] ?? '',
                        'estimated' => 1,
                        'source' => 'click_fallback',
                    ];
                    // Add device type for estimated open as well
                    if (isset($details['device'])) {
                        $estimatedOpen['device'] = $details['device'];
                    }
                    // Add location for estimated open as well
                    if (isset($details['country'])) {
                        $estimatedOpen['country'] = $details['country'];
                    }
                    if (isset($details['state'])) {
                        $estimatedOpen['state'] = $details['state'];
                    }
                    $stmt2 = $pdo->prepare("INSERT INTO events (campaign_id, event_type, details) VALUES (?,?,?)");
                    $stmt2->execute([$cid, 'open', json_encode($estimatedOpen)]);
                }
            } catch (Exception $e) {}
        }
    }

    if ($t === 'open') {
        header('Content-Type: image/gif');
        echo base64_decode('R0lGODlhAQABAPAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==');
        exit;
    }

    if ($t === 'click') {
        if (!empty($url) && preg_match('~^https?://~i', $url)) {
            // Link tracking enhancement: Replace {{email}} placeholder with actual recipient email
            // This allows tracking links to pass the recipient's email to destination URLs
            // Security: Validates email format and URL-encodes for safety
            if (!empty($rcpt) && strpos($url, '{{email}}') !== false) {
                // Validate email format to prevent malicious input
                if (filter_var($rcpt, FILTER_VALIDATE_EMAIL) !== false) {
                    // URL-encode the email to ensure it's safe for use in URL parameters
                    // This prevents injection attacks and ensures proper URL formatting
                    $url = str_replace('{{email}}', urlencode($rcpt), $url);
                }
                // If email is invalid, the placeholder remains in the URL (fault-tolerant)
            }
            header('Location: ' . $url, true, 302);
        } else {
            header('Content-Type: text/plain; charset=utf-8');
            echo "OK";
        }
        exit;
    }

    if ($t === 'unsubscribe') {
        header('Content-Type: text/html; charset=utf-8');
        echo "<!doctype html><html><head><meta charset='utf-8'><title>Unsubscribed</title></head><body style='font-family:system-ui, -apple-system, sans-serif;padding:24px;'>";
        echo "<h2>Unsubscribed</h2>";
        echo "<p>You have been unsubscribed. If this was a mistake, please contact the sender.</p>";
        echo "</body></html>";
        exit;
    }
}

if (isset($_GET['ajax']) && $_GET['ajax'] === 'campaign_stats' && isset($_GET['id'])) {
    $id = (int)$_GET['id'];
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode(get_campaign_stats($pdo, $id));
    // Close database connection before exit
    $pdo = null;
    exit;
}

// New API endpoint: Get time-series statistics for charts (all campaigns or specific campaign)
if (isset($_GET['ajax']) && $_GET['ajax'] === 'chart_data') {
    header('Content-Type: application/json; charset=utf-8');
    
    $campaignId = isset($_GET['id']) ? (int)$_GET['id'] : null;
    $days = isset($_GET['days']) ? (int)$_GET['days'] : DEFAULT_CHART_DAYS;
    
    // Validate days parameter (minimum 1, maximum MAX_CHART_DAYS)
    if ($days < 1 || $days > MAX_CHART_DAYS) {
        http_response_code(400);
        echo json_encode(['error' => 'Invalid days parameter. Must be between 1 and ' . MAX_CHART_DAYS . '.']);
        exit;
    }
    
    if ($campaignId) {
        // Single campaign time-series data
        $stmt = $pdo->prepare("
            SELECT 
                DATE(created_at) as date,
                event_type,
                COUNT(*) as count
            FROM events 
            WHERE campaign_id = ? AND created_at >= DATE_SUB(NOW(), INTERVAL ? DAY)
            GROUP BY DATE(created_at), event_type
            ORDER BY date ASC
        ");
        $stmt->execute([$campaignId, $days]);
    } else {
        // All campaigns time-series data
        $stmt = $pdo->prepare("
            SELECT 
                DATE(created_at) as date,
                event_type,
                COUNT(*) as count
            FROM events 
            WHERE created_at >= DATE_SUB(NOW(), INTERVAL ? DAY)
            GROUP BY DATE(created_at), event_type
            ORDER BY date ASC
        ");
        $stmt->execute([$days]);
    }
    
    // Organize data by date and event type
    $chartData = [];
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        $date = $row['date'];
        if (!isset($chartData[$date])) {
            $chartData[$date] = [
                'date' => $date,
                'delivered' => 0,
                'open' => 0,
                'click' => 0,
                'bounce' => 0,
                'unsubscribe' => 0,
                'spam' => 0
            ];
        }
        $eventType = $row['event_type'];
        // Handle known event types, log unknown ones
        if (isset($chartData[$date][$eventType])) {
            $chartData[$date][$eventType] = (int)$row['count'];
        } else {
            // Unknown event type - ignore for chart but could log if needed
            error_log("Unknown event type encountered in chart data: " . $eventType);
        }
    }
    
    echo json_encode(array_values($chartData));
    // Close database connection before exit
    $pdo = null;
    exit;
}

/**
 * Sanitize filename for HTTP header (RFC 6266 compliant)
 * Removes any characters that could cause header injection or path traversal
 * Note: Dots are replaced to prevent directory traversal attacks
 */
function sanitize_filename(string $filename): string {
    // Replace dots and any special characters with underscores
    return preg_replace('/[^a-zA-Z0-9_-]/', '_', $filename);
}

// New API endpoint: Download opened/clicked emails data
if (isset($_GET['action']) && $_GET['action'] === 'download_events') {
    $eventType = isset($_GET['type']) ? $_GET['type'] : 'open';
    $campaignId = isset($_GET['campaign_id']) ? (int)$_GET['campaign_id'] : null;
    
    // Validate event type
    if (!in_array($eventType, ['open', 'click'])) {
        http_response_code(400);
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode(['error' => 'Invalid event type. Must be "open" or "click".']);
        exit;
    }
    
    // Build query WITHOUT LIMIT - download all records as requested
    // Note: For very large datasets, this may take time but will complete
    
    if ($campaignId) {
        $stmt = $pdo->prepare("
            SELECT 
                e.campaign_id,
                c.name as campaign_name,
                e.event_type,
                e.created_at,
                JSON_UNQUOTE(JSON_EXTRACT(e.details, '$.rcpt')) as recipient,
                JSON_UNQUOTE(JSON_EXTRACT(e.details, '$.ip')) as ip_address,
                JSON_UNQUOTE(JSON_EXTRACT(e.details, '$.ua')) as user_agent,
                JSON_UNQUOTE(JSON_EXTRACT(e.details, '$.device')) as device_type,
                JSON_UNQUOTE(JSON_EXTRACT(e.details, '$.country')) as country,
                JSON_UNQUOTE(JSON_EXTRACT(e.details, '$.state')) as state,
                JSON_UNQUOTE(JSON_EXTRACT(e.details, '$.url')) as click_url
            FROM events e
            LEFT JOIN campaigns c ON e.campaign_id = c.id
            WHERE e.event_type = ? AND e.campaign_id = ?
            ORDER BY e.created_at DESC
        ");
        $stmt->execute([$eventType, $campaignId]);
        // Sanitize filename - dots will be replaced with underscores
        $filename = sanitize_filename("campaign_{$campaignId}_{$eventType}_" . date('Y-m-d')) . '.csv';
    } else {
        $stmt = $pdo->prepare("
            SELECT 
                e.campaign_id,
                c.name as campaign_name,
                e.event_type,
                e.created_at,
                JSON_UNQUOTE(JSON_EXTRACT(e.details, '$.rcpt')) as recipient,
                JSON_UNQUOTE(JSON_EXTRACT(e.details, '$.ip')) as ip_address,
                JSON_UNQUOTE(JSON_EXTRACT(e.details, '$.ua')) as user_agent,
                JSON_UNQUOTE(JSON_EXTRACT(e.details, '$.device')) as device_type,
                JSON_UNQUOTE(JSON_EXTRACT(e.details, '$.country')) as country,
                JSON_UNQUOTE(JSON_EXTRACT(e.details, '$.state')) as state,
                JSON_UNQUOTE(JSON_EXTRACT(e.details, '$.url')) as click_url
            FROM events e
            LEFT JOIN campaigns c ON e.campaign_id = c.id
            WHERE e.event_type = ?
            ORDER BY e.created_at DESC
        ");
        $stmt->execute([$eventType]);
        // Sanitize filename - dots will be replaced with underscores
        $filename = sanitize_filename("all_campaigns_{$eventType}_" . date('Y-m-d')) . '.csv';
    }
    
    // Set headers for CSV download
    header('Content-Type: text/csv; charset=utf-8');
    header('Content-Disposition: attachment; filename="' . $filename . '"');
    
    // Output CSV using streaming to avoid memory issues
    $output = fopen('php://output', 'w');
    
    // CSV headers - now includes device type, location and URL
    fputcsv($output, ['Campaign ID', 'Campaign Name', 'Event Type', 'Date/Time', 'Recipient Email', 'IP Address', 'User Agent', 'Device Type', 'Country', 'State/Region', 'Click URL']);
    
    // CSV data - stream results one at a time
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        fputcsv($output, [
            $row['campaign_id'],
            $row['campaign_name'],
            $row['event_type'],
            $row['created_at'],
            $row['recipient'],
            $row['ip_address'] ?? '',
            $row['user_agent'] ?? '',
            $row['device_type'] ?? '',
            $row['country'] ?? '',
            $row['state'] ?? '',
            $row['click_url'] ?? ''
        ]);
    }
    
    fclose($output);
    exit;
}

///////////////////////
//  ACTIONS
///////////////////////
$action = $_POST['action'] ?? null;
$page   = $_GET['page'] ?? 'list';

if ($action === 'check_connection_profile') {
    $pid = (int)($_POST['profile_id'] ?? 0);
    header('Content-Type: application/json; charset=utf-8');

    if ($pid <= 0) {
        echo json_encode(['ok'=>false,'msg'=>'Invalid profile id']);
        exit;
    }

    $stmt = $pdo->prepare("SELECT * FROM sending_profiles WHERE id = ? LIMIT 1");
    $stmt->execute([$pid]);
    $profile = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!$profile) {
        echo json_encode(['ok'=>false,'msg'=>'Profile not found']);
        exit;
    }

    try {
        if (($profile['type'] ?? 'smtp') === 'api') {
            $res = api_check_connection($profile);
        } else {
            $res = smtp_check_connection($profile);
        }
        echo json_encode($res);
        exit;
    } catch (Exception $e) {
        echo json_encode(['ok'=>false,'msg'=>$e->getMessage()]);
        exit;
    }
}

// AJAX endpoint to test proxy and return the outbound IP
if ($action === 'check_proxy') {
    $pid = (int)($_POST['profile_id'] ?? 0);
    header('Content-Type: application/json; charset=utf-8');

    if ($pid <= 0) {
        echo json_encode(['ok' => false, 'msg' => 'Invalid profile id']);
        exit;
    }

    $stmt = $pdo->prepare("SELECT * FROM sending_profiles WHERE id = ? LIMIT 1");
    $stmt->execute([$pid]);
    $profile = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!$profile) {
        echo json_encode(['ok' => false, 'msg' => 'Profile not found']);
        exit;
    }

    if (empty($profile['proxy_enabled'])) {
        echo json_encode(['ok' => false, 'msg' => 'Proxy is not enabled for this profile']);
        exit;
    }

    $ch = curl_init('https://api.ipify.org?format=json');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 15);
    apply_proxy_to_curl($ch, $profile);

    $resp = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err  = curl_error($ch);
    curl_close($ch);

    if ($resp === false || $code < 200 || $code >= 300) {
        $errMsg = $err !== '' ? $err : 'HTTP ' . $code;
        echo json_encode(['ok' => false, 'msg' => 'Proxy test failed: ' . $errMsg]);
        exit;
    }

    $data = json_decode($resp, true);
    $ip   = $data['ip'] ?? 'unknown';
    echo json_encode(['ok' => true, 'msg' => 'Proxy is working. Outbound IP: ' . $ip, 'ip' => $ip]);
    exit;
}

// AJAX endpoint to test proxy with live form values (before saving)
if ($action === 'test_proxy_live') {
    header('Content-Type: application/json; charset=utf-8');

    $proxy_type = in_array(trim($_POST['proxy_type'] ?? 'http'), ['http', 'socks4', 'socks5'], true)
        ? trim($_POST['proxy_type'])
        : 'http';
    $proxy_host = trim($_POST['proxy_host'] ?? '');
    $proxy_port = max(1, min(65535, (int)($_POST['proxy_port'] ?? 8080)));
    $proxy_user = trim($_POST['proxy_user'] ?? '');
    $proxy_pass = (string)($_POST['proxy_pass'] ?? '');

    if ($proxy_host === '') {
        echo json_encode(['ok' => false, 'msg' => 'Proxy host is required']);
        exit;
    }

    $profile = [
        'proxy_enabled' => 1,
        'proxy_type'    => $proxy_type,
        'proxy_host'    => $proxy_host,
        'proxy_port'    => $proxy_port,
        'proxy_user'    => $proxy_user,
        'proxy_pass'    => $proxy_pass,
    ];

    $ch = curl_init('https://api.ipify.org?format=json');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 15);
    apply_proxy_to_curl($ch, $profile);

    $resp = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err  = curl_error($ch);
    curl_close($ch);

    if ($resp === false || $code < 200 || $code >= 300) {
        $errMsg = $err !== '' ? $err : 'HTTP ' . $code;
        echo json_encode(['ok' => false, 'msg' => 'Proxy test failed: ' . $errMsg]);
        exit;
    }

    $data = json_decode($resp, true);
    $ip   = $data['ip'] ?? 'unknown';
    echo json_encode(['ok' => true, 'msg' => 'Proxy is working. Outbound IP: ' . $ip, 'ip' => $ip]);
    exit;
}

// AJAX endpoint for real-time campaign progress
if ($action === 'get_campaign_progress') {
    $cid = (int)($_GET['campaign_id'] ?? $_POST['campaign_id'] ?? 0);
    header('Content-Type: application/json; charset=utf-8');
    
    if ($cid <= 0) {
        echo json_encode(['ok'=>false,'error'=>'Invalid campaign ID']);
        exit;
    }
    
    try {
        $campaign = get_campaign($pdo, $cid);
        if (!$campaign) {
            echo json_encode(['ok'=>false,'error'=>'Campaign not found']);
            exit;
        }
        
        // OPTIMIZED: Get event counts using indexed query (campaign_id, event_type index)
        // This query is now significantly faster with the idx_campaign_event index
        $stmt = $pdo->prepare("
            SELECT 
                SUM(CASE WHEN event_type = 'delivered' THEN 1 ELSE 0 END) as delivered,
                SUM(CASE WHEN event_type = 'bounce' THEN 1 ELSE 0 END) as bounced,
                SUM(CASE WHEN event_type = 'deferred' THEN 1 ELSE 0 END) as deferred,
                SUM(CASE WHEN event_type = 'skipped_unsubscribe' THEN 1 ELSE 0 END) as skipped,
                COUNT(*) as total_events
            FROM events USE INDEX (idx_campaign_event)
            WHERE campaign_id = ?
            AND event_type IN ('delivered', 'bounce', 'deferred', 'skipped_unsubscribe')
        ");
        $stmt->execute([$cid]);
        $stats = $stmt->fetch(PDO::FETCH_ASSOC);
        
        // CRITICAL FIX: Improved active worker detection with multiple event types
        // Now checks for worker_start, worker_heartbeat, delivered, bounce, and deferred events
        // Extended window from 30s to 60s to account for slower processing
        $stmt = $pdo->prepare("
            SELECT COUNT(DISTINCT JSON_EXTRACT(details, '$.worker_pid')) as active_workers
            FROM events USE INDEX (idx_campaign_created)
            WHERE campaign_id = ?
            AND JSON_EXTRACT(details, '$.worker_pid') IS NOT NULL
            AND event_type IN ('worker_start', 'worker_heartbeat', 'delivered', 'bounce', 'deferred')
            AND created_at >= DATE_SUB(NOW(), INTERVAL ? SECOND)
        ");
        $stmt->execute([$cid, WORKER_HEARTBEAT_WINDOW]);
        $workerCount = (int)$stmt->fetchColumn();
        
        $response = [
            'ok' => true,
            'campaign_id' => $cid,
            'status' => $campaign['status'],
            'progress_status' => $campaign['progress_status'] ?? 'draft',
            'total_recipients' => (int)$campaign['total_recipients'],
            'progress_sent' => (int)$campaign['progress_sent'],
            'progress_total' => (int)$campaign['progress_total'],
            'delivered' => (int)($stats['delivered'] ?? 0),
            'bounced' => (int)($stats['bounced'] ?? 0),
            'deferred' => (int)($stats['deferred'] ?? 0),
            'skipped' => (int)($stats['skipped'] ?? 0),
            'active_workers' => $workerCount,
            'percentage' => 0,
        ];
        
        // Use max(progress_sent, event_count) for the progress display.
        // progress_sent is updated atomically once per SMTP batch (one cheap UPDATE
        // per worker), so it reflects all 117 workers' completions nearly simultaneously.
        // total_events is the slower event-row count that lags behind during heavy insert.
        $displayProcessed = max((int)$campaign['progress_sent'], (int)($stats['total_events'] ?? 0));
        $response['total_processed'] = $displayProcessed;
        
        if ($response['total_recipients'] > 0) {
            $response['percentage'] = round(($displayProcessed / $response['total_recipients']) * 100, 1);
        }
        
        echo json_encode($response);
        exit;
    } catch (Exception $e) {
        echo json_encode(['ok'=>false,'error'=>$e->getMessage()]);
        exit;
    }
}

if ($action === 'create_campaign') {
    $name = trim($_POST['name'] ?? '');
    if ($name === '') {
        $name = 'New Single Send';
    }
    $stmt = $pdo->prepare("
        INSERT INTO campaigns (name, subject, preheader, status)
        VALUES (?, '', '', 'draft')
    ");
    $stmt->execute([$name]);
    $newId = (int)$pdo->lastInsertId();
    header("Location: ?page=editor&id={$newId}");
    exit;
}

if ($action === 'duplicate_campaign') {
    $cid = (int)($_POST['campaign_id'] ?? 0);
    if ($cid > 0) {
        try {
            $orig = get_campaign($pdo, $cid);
            if ($orig) {
                $newName = $orig['name'] . ' (Copy)';
                $stmt = $pdo->prepare("
                    INSERT INTO campaigns (name, subject, preheader, from_email, html, unsubscribe_enabled, ref_injection_enabled, preheader_injection_enabled, footer_injection_enabled, sender_name, status)
                    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'draft')
                ");
                $htmlToStore = is_string($orig['html']) ? ('BASE64:' . base64_encode($orig['html'])) : '';
                $stmt->execute([
                    $newName,
                    $orig['subject'] ?? '',
                    $orig['preheader'] ?? '',
                    $orig['from_email'] ?? '',
                    $htmlToStore,
                    $orig['unsubscribe_enabled'] ?? 0,
                    $orig['ref_injection_enabled'] ?? 1,
                    $orig['preheader_injection_enabled'] ?? 1,
                    $orig['footer_injection_enabled'] ?? 1,
                    $orig['sender_name'] ?? '',
                ]);
                $newId = (int)$pdo->lastInsertId();
                header("Location: ?page=editor&id={$newId}");
                exit;
            }
        } catch (Exception $e) {}
    }
    header("Location: ?page=list");
    exit;
}

if ($action === 'bulk_campaigns') {
    $ids = $_POST['campaign_ids'] ?? [];
    $bulk_action = $_POST['bulk_action'] ?? '';
    if (!is_array($ids) || empty($ids)) {
        header("Location: ?page=list");
        exit;
    }
    $ids = array_map('intval', $ids);
    if ($bulk_action === 'delete_selected') {
        foreach ($ids as $id) {
            try {
                $stmt = $pdo->prepare("DELETE FROM events WHERE campaign_id = ?");
                $stmt->execute([$id]);
                $stmt = $pdo->prepare("DELETE FROM campaigns WHERE id = ?");
                $stmt->execute([$id]);
            } catch (Exception $e) {}
        }
    } elseif ($bulk_action === 'duplicate_selected') {
        foreach ($ids as $id) {
            try {
                $orig = get_campaign($pdo, $id);
                if ($orig) {
                    $newName = $orig['name'] . ' (Copy)';
                    $stmt = $pdo->prepare("
                        INSERT INTO campaigns (name, subject, preheader, from_email, html, unsubscribe_enabled, ref_injection_enabled, preheader_injection_enabled, footer_injection_enabled, sender_name, status)
                        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'draft')
                    ");
                    $htmlToStore = is_string($orig['html']) ? ('BASE64:' . base64_encode($orig['html'])) : '';
                    $stmt->execute([
                        $newName,
                        $orig['subject'] ?? '',
                        $orig['preheader'] ?? '',
                        $orig['from_email'] ?? '',
                        $htmlToStore,
                        $orig['unsubscribe_enabled'] ?? 0,
                        $orig['ref_injection_enabled'] ?? 1,
                        $orig['preheader_injection_enabled'] ?? 1,
                        $orig['footer_injection_enabled'] ?? 1,
                        $orig['sender_name'] ?? '',
                    ]);
                }
            } catch (Exception $e) {}
        }
    }
    header("Location: ?page=list");
    exit;
}

if ($action === 'delete_unsubscribes') {
    // Option A: delete all unsubscribes
    try {
        $pdo->beginTransaction();
        $pdo->exec("DELETE FROM unsubscribes");
        $pdo->commit();
    } catch (Exception $e) {
        try { $pdo->rollBack(); } catch (Exception $_) {}
    }
    header("Location: ?page=activity&cleared_unsubscribes=1");
    exit;
}

if ($action === 'delete_bounces') {
    // Delete all events of type 'bounce'
    try {
        $pdo->beginTransaction();
        $stmt = $pdo->prepare("DELETE FROM events WHERE event_type = ?");
        $stmt->execute(['bounce']);
        $pdo->commit();
    } catch (Exception $e) {
        try { $pdo->rollBack(); } catch (Exception $_) {}
    }
    header("Location: ?page=activity&cleared_bounces=1");
    exit;
}

if ($action === 'delete_campaign') {
    $cid = (int)($_POST['campaign_id'] ?? 0);
    if ($cid > 0) {
        try {
            $stmt = $pdo->prepare("DELETE FROM events WHERE campaign_id = ?");
            $stmt->execute([$cid]);
            $stmt = $pdo->prepare("DELETE FROM campaigns WHERE id = ?");
            $stmt->execute([$cid]);
        } catch (Exception $e) {
            // ignore
        }
    }
    header("Location: ?page=list");
    exit;
}

if ($action === 'save_campaign') {
    $id        = (int)($_POST['id'] ?? 0);
    $subject   = trim($_POST['subject'] ?? '');
    $preheader = trim($_POST['preheader'] ?? '');
    $audience  = trim($_POST['audience'] ?? '');
    $html      = $_POST['html'] ?? '';

    $rot       = get_rotation_settings($pdo);
    $rotOn     = (int)$rot['rotation_enabled'] === 1;

    if ($rotOn) {
        $existing   = get_campaign($pdo, $id);
        $fromEmail  = $existing ? $existing['from_email'] : '';
    } else {
        $fromEmail  = trim($_POST['from_email'] ?? '');
    }

    $unsubscribe_enabled         = isset($_POST['unsubscribe_enabled'])         ? 1 : 0;
    $preheader_injection_enabled = isset($_POST['preheader_injection_enabled']) ? 1 : 0;
    $footer_injection_enabled    = isset($_POST['footer_injection_enabled'])    ? 1 : 0;
    $sender_name = trim($_POST['sender_name'] ?? '');
    $reply_to = trim($_POST['reply_to'] ?? '');

    $htmlToStore = 'BASE64:' . base64_encode($html);

    try {
        $stmt = $pdo->prepare("
            UPDATE campaigns
            SET subject = ?, preheader = ?, from_email = ?, html = ?, unsubscribe_enabled = ?,
                preheader_injection_enabled = ?, footer_injection_enabled = ?, sender_name = ?, reply_to = ?
            WHERE id = ?
        ");
        $stmt->execute([$subject, $preheader, $fromEmail, $htmlToStore, $unsubscribe_enabled,
                        $preheader_injection_enabled, $footer_injection_enabled, $sender_name, $reply_to, $id]);
    } catch (Exception $e) {
        error_log("Failed to save campaign id {$id}: " . $e->getMessage());
        if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) || strpos($_SERVER['CONTENT_TYPE'] ?? '', 'application/json') !== false) {
            http_response_code(500);
            echo json_encode(['error' => 'Failed to save campaign']);
            exit;
        }
        header("Location: ?page=editor&id={$id}&save_error=1");
        exit;
    }

    if (isset($_POST['go_to_review'])) {
        header("Location: ?page=review&id={$id}");
    } else {
        if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) || strpos($_SERVER['CONTENT_TYPE'] ?? '', 'application/json') !== false) {
            header('Content-Type: application/json; charset=utf-8');
            echo json_encode(['ok' => 1, 'id' => $id]);
            exit;
        }
        header("Location: ?page=editor&id={$id}&saved=1");
    }
    exit;
}

if ($action === 'send_test_message') {
    $id = (int)($_POST['id'] ?? 0);
    $addresses = trim($_POST['test_addresses'] ?? '');
    $campaign = get_campaign($pdo, $id);
    if ($campaign && $addresses !== '') {
        $toUpdate = false;
        $updFields = [];
        $updVals = [];

        if (isset($_POST['subject'])) {
            $toUpdate = true;
            $updFields[] = "subject = ?";
            $updVals[] = trim($_POST['subject']);
        }
        if (isset($_POST['preheader'])) {
            $toUpdate = true;
            $updFields[] = "preheader = ?";
            $updVals[] = trim($_POST['preheader']);
        }
        if (isset($_POST['sender_name'])) {
            $toUpdate = true;
            $updFields[] = "sender_name = ?";
            $updVals[] = trim($_POST['sender_name']);
        }
        $unsubscribe_enabled = isset($_POST['unsubscribe_enabled']) ? 1 : 0;
        if ($unsubscribe_enabled != ($campaign['unsubscribe_enabled'] ?? 0)) {
            $toUpdate = true;
            $updFields[] = "unsubscribe_enabled = ?";
            $updVals[] = $unsubscribe_enabled;
        }
        $ref_injection_enabled = isset($_POST['ref_injection_enabled']) ? 1 : 0;
        if ($ref_injection_enabled != ($campaign['ref_injection_enabled'] ?? 1)) {
            $toUpdate = true;
            $updFields[] = "ref_injection_enabled = ?";
            $updVals[] = $ref_injection_enabled;
        }
        $preheader_injection_enabled = isset($_POST['preheader_injection_enabled']) ? 1 : 0;
        if ($preheader_injection_enabled != ($campaign['preheader_injection_enabled'] ?? 1)) {
            $toUpdate = true;
            $updFields[] = "preheader_injection_enabled = ?";
            $updVals[] = $preheader_injection_enabled;
        }
        $footer_injection_enabled = isset($_POST['footer_injection_enabled']) ? 1 : 0;
        if ($footer_injection_enabled != ($campaign['footer_injection_enabled'] ?? 1)) {
            $toUpdate = true;
            $updFields[] = "footer_injection_enabled = ?";
            $updVals[] = $footer_injection_enabled;
        }

        if ($toUpdate && !empty($updFields)) {
            $updVals[] = $id;
            $sql = "UPDATE campaigns SET " . implode(',', $updFields) . " WHERE id = ?";
            $stmt = $pdo->prepare($sql);
            $stmt->execute($updVals);
            $campaign = get_campaign($pdo, $id);
        }

        $parts = array_filter(array_map('trim', preg_split("/\r\n|\n|\r|,/", $addresses)));
        $valid = [];
        foreach ($parts as $p) {
            if (filter_var($p, FILTER_VALIDATE_EMAIL)) $valid[] = $p;
        }
        if (!empty($valid)) {
            $recipientsText = implode("\n", $valid);

            session_write_close();

            if (function_exists('fastcgi_finish_request')) {
                header("Location: ?page=review&id={$id}&test_sent=1");
                echo "<!doctype html><html><body>Sending test in background... Redirecting.</body></html>";
                @ob_end_flush();
                @flush();
                fastcgi_finish_request();

                try {
                    send_campaign_real($pdo, $campaign, $recipientsText, true);
                } catch (Exception $e) {
                } finally {
                    // CRITICAL: Close database connection before exit to prevent "too many connections"
                    $pdo = null;
                }
                exit;
            }

            // fallback: try spawn; if spawn fails do synchronous send (web request will wait)
            $spawned = spawn_background_send($pdo, $id, $recipientsText);
            if (!$spawned) {
                // synchronous fallback (guarantees it actually sends)
                ignore_user_abort(true);
                set_time_limit(0);
                try {
                    send_campaign_real($pdo, $campaign, $recipientsText, true);
                } catch (Exception $e) {}
            }
        }
    }
    // CRITICAL: Close database connection before exit to prevent "too many connections"
    $pdo = null;
    header("Location: ?page=review&id={$id}&test_sent=1");
    exit;
}

// CRITICAL FIX: Manual recovery action for stuck campaigns
// Allows manual completion of campaigns that are stuck showing "0 workers"
if ($action === 'recover_stuck_campaign') {
    $id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
    if ($id > 0) {
        try {
            // Get campaign details
            $stmt = $pdo->prepare("SELECT id, status, progress_sent, total_recipients FROM campaigns WHERE id = ?");
            $stmt->execute([$id]);
            $campaign = $stmt->fetch(PDO::FETCH_ASSOC);
            
            if ($campaign) {
                $progress = (int)$campaign['progress_sent'];
                $total = (int)$campaign['total_recipients'];
                $status = $campaign['status'];
                
                // Only recover campaigns that are stuck in "sending" status
                if ($status === 'sending') {
                    // Check if there are any active workers (last 60 seconds)
                    $stmt = $pdo->prepare("
                        SELECT COUNT(DISTINCT JSON_EXTRACT(details, '$.worker_pid')) as active_workers
                        FROM events
                        WHERE campaign_id = ?
                        AND JSON_EXTRACT(details, '$.worker_pid') IS NOT NULL
                        AND event_type IN ('worker_start', 'worker_heartbeat', 'delivered', 'bounce', 'deferred')
                        AND created_at >= DATE_SUB(NOW(), INTERVAL 60 SECOND)
                    ");
                    $stmt->execute([$id]);
                    $activeWorkers = (int)$stmt->fetchColumn();
                    
                    if ($activeWorkers === 0 && $progress > 0) {
                        // No active workers and has made progress - safe to mark as complete
                        $stmt = $pdo->prepare("
                            UPDATE campaigns 
                            SET status = 'sent',
                                sent_at = COALESCE(sent_at, NOW()),
                                progress_status = 'completed'
                            WHERE id = ?
                        ");
                        $stmt->execute([$id]);
                        
                        // Log recovery event
                        $ins = $pdo->prepare("INSERT INTO events (campaign_id, event_type, details) VALUES (?,?,?)");
                        $ins->execute([
                            $id,
                            'campaign_recovered',
                            json_encode([
                                'progress_sent' => $progress,
                                'total_recipients' => $total,
                                'recovered_at' => date('Y-m-d H:i:s'),
                                'reason' => 'Manual recovery - no active workers detected'
                            ])
                        ]);
                        
                        echo json_encode(['ok' => true, 'message' => "Campaign recovered successfully. Sent: $progress emails."]);
                    } else if ($activeWorkers > 0) {
                        echo json_encode(['ok' => false, 'message' => "Campaign has $activeWorkers active workers. Cannot recover while workers are running."]);
                    } else {
                        echo json_encode(['ok' => false, 'message' => 'Campaign has not made any progress. Cannot recover.']);
                    }
                } else {
                    echo json_encode(['ok' => false, 'message' => "Campaign is not stuck. Status: $status"]);
                }
            } else {
                echo json_encode(['ok' => false, 'message' => 'Campaign not found']);
            }
        } catch (Exception $e) {
            echo json_encode(['ok' => false, 'message' => 'Recovery failed: ' . $e->getMessage()]);
        }
    } else {
        echo json_encode(['ok' => false, 'message' => 'Invalid campaign ID']);
    }
    exit;
}

if ($action === 'send_campaign') {
    $id = (int)($_POST['id'] ?? 0);
    $testRecipients = $_POST['test_recipients'] ?? '';
    // Handle both single value (backward compatibility) and array (new multiple selection)
    $audience_select_raw = $_POST['audience_select'] ?? '';
    $audience_selections = is_array($audience_select_raw) ? $audience_select_raw : [trim($audience_select_raw)];
    $campaign = get_campaign($pdo, $id);
    if ($campaign) {
        // Guard: prevent re-sending a campaign that is already in progress or completed.
        // Without this check, submitting the send form on a sent/sending/queued campaign
        // would re-queue and re-deliver all emails to all recipients a second time.
        if (in_array($campaign['status'] ?? 'draft', ['sent', 'sending', 'queued'])) {
            header("Location: ?page=stats&id={$id}");
            exit;
        }

        $toUpdate = false;
        $updFields = [];
        $updVals = [];

        if (isset($_POST['subject'])) {
            $toUpdate = true;
            $updFields[] = "subject = ?";
            $updVals[] = trim($_POST['subject']);
        }
        if (isset($_POST['preheader'])) {
            $toUpdate = true;
            $updFields[] = "preheader = ?";
            $updVals[] = trim($_POST['preheader']);
        }
        if (isset($_POST['sender_name'])) {
            $toUpdate = true;
            $updFields[] = "sender_name = ?";
            $updVals[] = trim($_POST['sender_name']);
        }
        $unsubscribe_enabled = isset($_POST['unsubscribe_enabled']) ? 1 : 0;
        if ($unsubscribe_enabled != ($campaign['unsubscribe_enabled'] ?? 0)) {
            $toUpdate = true;
            $updFields[] = "unsubscribe_enabled = ?";
            $updVals[] = $unsubscribe_enabled;
        }
        $ref_injection_enabled = isset($_POST['ref_injection_enabled']) ? 1 : 0;
        if ($ref_injection_enabled != ($campaign['ref_injection_enabled'] ?? 1)) {
            $toUpdate = true;
            $updFields[] = "ref_injection_enabled = ?";
            $updVals[] = $ref_injection_enabled;
        }
        $preheader_injection_enabled = isset($_POST['preheader_injection_enabled']) ? 1 : 0;
        if ($preheader_injection_enabled != ($campaign['preheader_injection_enabled'] ?? 1)) {
            $toUpdate = true;
            $updFields[] = "preheader_injection_enabled = ?";
            $updVals[] = $preheader_injection_enabled;
        }
        $footer_injection_enabled = isset($_POST['footer_injection_enabled']) ? 1 : 0;
        if ($footer_injection_enabled != ($campaign['footer_injection_enabled'] ?? 1)) {
            $toUpdate = true;
            $updFields[] = "footer_injection_enabled = ?";
            $updVals[] = $footer_injection_enabled;
        }

        // If rotation is OFF, allow overriding the campaign.from_email from the Review form select
        $rotSettings = get_rotation_settings($pdo);
        $rotationEnabled = (int)$rotSettings['rotation_enabled'] === 1;
        if (!$rotationEnabled && isset($_POST['from_email'])) {
            $fromOverride = trim($_POST['from_email']);
            if ($fromOverride !== '') {
                $toUpdate = true;
                $updFields[] = "from_email = ?";
                $updVals[] = $fromOverride;
                $campaign['from_email'] = $fromOverride; // keep in-memory
            }
        }

        if ($toUpdate && !empty($updFields)) {
            $updVals[] = $id;
            $sql = "UPDATE campaigns SET " . implode(',', $updFields) . " WHERE id = ?";
            $stmt = $pdo->prepare($sql);
            $stmt->execute($updVals);
            $campaign = get_campaign($pdo, $id);
        }

        // Set initial status to 'queued' and increase memory limit for large campaigns
        try {
            $stmt = $pdo->prepare("UPDATE campaigns SET status='queued' WHERE id=?");
            $stmt->execute([$campaign['id']]);
        } catch (Exception $e) {
            error_log("Failed to set campaign status to queued: " . $e->getMessage());
        }

        // Increase memory limit for large multi-list campaigns to 8GB (already set at script start, but ensure it's active)
        $currentLimit = ini_get('memory_limit');
        $currentLimitBytes = return_bytes($currentLimit);
        $targetBytes = return_bytes(MEMORY_LIMIT_LARGE_SERVER);
        
        // Ensure we have 8GB for large campaign processing
        if ($currentLimit !== '-1' && $currentLimitBytes < $targetBytes) {
            @ini_set('memory_limit', MEMORY_LIMIT_LARGE_SERVER);
            error_log("Campaign {$campaign['id']}: Increased memory limit from $currentLimit to " . MEMORY_LIMIT_LARGE_SERVER . " for multi-list processing");
        } else {
            error_log("Campaign {$campaign['id']}: Using memory limit of $currentLimit for processing " . count($audience_selections) . " list(s)");
        }

        $allRecipients = [];
        $hasManualSelection = false;

        // Process all selected audiences (multiple lists and/or manual)
        foreach ($audience_selections as $audience_select) {
            $audience_select = trim($audience_select);
            if ($audience_select === '') continue;
            
            if (strpos($audience_select, 'list:') === 0) {
                // Process list selection
                $lid = (int)substr($audience_select, strlen('list:'));
                if ($lid > 0) {
                    // Single JOIN query: fetch all valid emails that are NOT in the unsubscribes
                    // table in one round-trip. Replaces N individual is_unsubscribed() DB calls
                    // (which would timeout PHP's max_execution_time for lists > ~10k emails).
                    $stmt = $pdo->prepare(
                        "SELECT c.email FROM contacts c
                         LEFT JOIN unsubscribes u ON u.email = c.email
                         WHERE c.list_id = ? AND c.email != '' AND u.email IS NULL"
                    );
                    $stmt->execute([$lid]);
                    $listRecipientCount = 0;
                    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                        $em = trim((string)($row['email'] ?? ''));
                        if ($em === '' || !filter_var($em, FILTER_VALIDATE_EMAIL)) continue;
                        $allRecipients[] = strtolower($em);
                        $listRecipientCount++;
                    }
                    error_log("Campaign {$campaign['id']}: Collected $listRecipientCount valid recipients from list $lid");
                }
            } elseif ($audience_select === 'manual') {
                // Mark that manual was selected
                $hasManualSelection = true;
            }
        }
        
        // If manual option was selected, add manual recipients
        if ($hasManualSelection) {
            $parts = array_filter(array_map('trim', preg_split("/\r\n|\n|\r|,/", $testRecipients)));
            foreach ($parts as $p) {
                if (!filter_var($p, FILTER_VALIDATE_EMAIL)) continue;
                if (is_unsubscribed($pdo, $p)) continue;
                $allRecipients[] = strtolower($p);
            }
        }

        $allRecipients = array_values(array_unique($allRecipients));

        if (empty($allRecipients)) {
            // Update status back to draft if no recipients
            try {
                $stmt = $pdo->prepare("UPDATE campaigns SET status='draft' WHERE id=?");
                $stmt->execute([$campaign['id']]);
            } catch (Exception $e) {}
            header("Location: ?page=review&id={$id}&no_recipients=1");
            exit;
        }

        // Log recipient collection completion for large campaigns
        $totalRecipients = count($allRecipients);
        if ($totalRecipients >= 10000) {
            error_log("Campaign {$campaign['id']}: Successfully collected $totalRecipients recipients from " . count($audience_selections) . " list(s)");
        }

        // Update total_recipients but keep status as 'queued' - will be changed to 'sending' by send function
        try {
            $stmt = $pdo->prepare("UPDATE campaigns SET total_recipients=? WHERE id=?");
            $stmt->execute([$totalRecipients, $campaign['id']]);
        } catch (Exception $e) {
            error_log("Failed to update total_recipients: " . $e->getMessage());
        }

        $recipientsTextFull = implode("\n", $allRecipients);

        $redirect = "?page=list&sent=1&sent_campaign=" . (int)$campaign['id'];

        session_write_close();

        $rotSettings = get_rotation_settings($pdo);
        $rotationEnabled = (int)$rotSettings['rotation_enabled'] === 1;
        $activeProfiles = array_values(array_filter(get_profiles($pdo), function($p){ return (int)$p['active'] === 1; }));

        // Check if we should use multi-profile rotation or single profile
        if ($rotationEnabled && count($activeProfiles) > 1) {
            // ROTATION MODE: Use ALL active profiles and distribute emails among them
            // Use fastcgi_finish_request if available to send in background
            if (function_exists('fastcgi_finish_request')) {
                header("Location: $redirect");
                echo "<!doctype html><html><body>Sending in background... Redirecting.</body></html>";
                @ob_end_flush();
                @flush();
                fastcgi_finish_request();

                ignore_user_abort(true);
                set_time_limit(0);

                // Send using multi-profile rotation function
                try {
                    send_campaign_with_rotation($pdo, $campaign, $allRecipients, $activeProfiles, false);
                } catch (Throwable $e) {
                    error_log("Campaign send error (rotation fastcgi): " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine());
                    error_log("Stack trace: " . $e->getTraceAsString());
                } finally {
                    // CRITICAL: Close database connection before exit to prevent "too many connections"
                    $pdo = null;
                }
                exit;
            }

            // Fallback: send synchronously (blocking)
            header("Location: $redirect");
            echo "<!doctype html><html><body>Sending campaign... Please wait.</body></html>";
            @ob_end_flush();
            @flush();

            ignore_user_abort(true);
            set_time_limit(0);

            try {
                send_campaign_with_rotation($pdo, $campaign, $allRecipients, $activeProfiles, false);
            } catch (Throwable $e) {
                error_log("Campaign send error (rotation blocking): " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine());
                error_log("Stack trace: " . $e->getTraceAsString());
            } finally {
                // CRITICAL: Close database connection before exit to prevent "too many connections"
                $pdo = null;
            }
            exit;
        }

        // SINGLE PROFILE MODE: Use one profile (rotation disabled or only 1 active profile)
        // Determine which profile to use
        $profile = null;
        if (!empty($activeProfiles)) {
            // No rotation or only one profile: find profile matching campaign's from_email or use first active
            foreach ($activeProfiles as $p) {
                if (strtolower($p['from_email']) === strtolower($campaign['from_email'])) {
                    $profile = $p;
                    break;
                }
            }
            if (!$profile) $profile = $activeProfiles[0];
        }

        if (!$profile) {
            header("Location: ?page=review&id={$id}&send_err=no_profile");
            exit;
        }

        // Use fastcgi_finish_request if available to send in background
        if (function_exists('fastcgi_finish_request')) {
            header("Location: $redirect");
            echo "<!doctype html><html><body>Sending in background... Redirecting.</body></html>";
            @ob_end_flush();
            @flush();
            fastcgi_finish_request();

            ignore_user_abort(true);
            set_time_limit(0);

            // Send using new connection-based function
            try {
                send_campaign_with_connections($pdo, $campaign, $allRecipients, (int)$profile['id'], false);
            } catch (Exception $e) {
                error_log("Campaign send error: " . $e->getMessage());
            } finally {
                // CRITICAL: Close database connection before exit to prevent "too many connections"
                $pdo = null;
            }
            exit;
        }

        // Fallback: send synchronously (blocking)
        header("Location: $redirect");
        echo "<!doctype html><html><body>Sending campaign... Please wait.</body></html>";
        @ob_end_flush();
        @flush();

        ignore_user_abort(true);
        set_time_limit(0);

        try {
            send_campaign_with_connections($pdo, $campaign, $allRecipients, (int)$profile['id'], false);
        } catch (Exception $e) {
            error_log("Campaign send error: " . $e->getMessage());
        } finally {
            // CRITICAL: Close database connection before exit to prevent "too many connections"
            $pdo = null;
        }
        exit;

        // OLD COMPLEX LOGIC REMOVED - keeping below for reference but not used
        /*
        // If rotation enabled and multiple active profiles -> batch-wise distribution per profile with parallel workers
        if ($rotationEnabled && count($activeProfiles) > 0) {
            $globalDefaultBatch = max(1, (int)($rotSettings['batch_size'] ?? 1));
            $globalTargetPerMinute = max(1, (int)($rotSettings['target_per_minute'] ?? 1000));
            // Enforce MAX_WORKERS cap on rotation settings to prevent database connection exhaustion
            $globalWorkers = max(MIN_WORKERS, min(MAX_WORKERS, (int)($rotSettings['workers'] ?? DEFAULT_WORKERS)));
            
            // Log warning for large worker counts
            if ($globalWorkers >= WORKERS_WARNING_THRESHOLD) {
                error_log("Warning: Rotation settings requesting $globalWorkers workers (capped at MAX_WORKERS=" . MAX_WORKERS . ")");
            }
            
            $globalMessagesPerWorker = max(MIN_MESSAGES_PER_WORKER, (int)($rotSettings['messages_per_worker'] ?? DEFAULT_MESSAGES_PER_WORKER));

            // prepare profiles meta
            $profilesMeta = [];
            foreach ($activeProfiles as $i => $p) {
                $meta = [];
                $meta['profile'] = $p;
                $meta['batch_size'] = max(1, (int)($p['batch_size'] ?? $globalDefaultBatch));
                $meta['send_rate'] = max(0, (int)($p['send_rate'] ?? 0)); // msg / second (0 = no throttle)
                $meta['max_sends'] = max(0, (int)($p['max_sends'] ?? 0)); // 0 = unlimited
                $meta['used'] = max(0, (int)($p['sends_used'] ?? 0));
                $profilesMeta[] = $meta;
            }
            $pcount = count($profilesMeta);

            // compute default per-profile send_rate if not set (distribute globalTargetPerMinute)
            $perProfilePerMin = (int)ceil($globalTargetPerMinute / max(1, $pcount));
            $defaultPerProfilePerSec = max(1, (int)ceil($perProfilePerMin / 60));

            // Distribution algorithm: sequentially assign profile.batch_size per profile, skipping profiles that reached max_sends
            $parts = array_fill(0, $pcount, []);
            $n = count($allRecipients);
            $idx = 0; // pointer into recipients
            $pi = 0;  // profile index pointer
            $roundsWithoutAssign = 0;
            while ($idx < $n) {
                $pm = &$profilesMeta[$pi];
                // remaining capacity for this profile
                if ($pm['max_sends'] > 0) {
                    $capacity = max(0, $pm['max_sends'] - $pm['used']);
                } else {
                    $capacity = PHP_INT_MAX;
                }

                if ($capacity <= 0) {
                    // skip this profile (can't accept more)
                    $pi = ($pi + 1) % $pcount;
                    $roundsWithoutAssign++;
                    if ($roundsWithoutAssign >= $pcount) {
                        // no profile can accept more sends -> break to avoid infinite loop
                        break;
                    }
                    continue;
                }

                $toTake = min($pm['batch_size'], $capacity, $n - $idx);
                $slice = array_slice($allRecipients, $idx, $toTake);
                if (!empty($slice)) {
                    $parts[$pi] = array_merge($parts[$pi], $slice);
                    $pm['used'] += count($slice);
                    $idx += count($slice);
                    $roundsWithoutAssign = 0;
                } else {
                    $roundsWithoutAssign++;
                }

                // move to next profile (respect mode)
                if (($rotSettings['mode'] ?? 'sequential') === 'random') {
                    $pi = rand(0, $pcount - 1);
                } else {
                    $pi = ($pi + 1) % $pcount;
                }
            }
            // If some recipients remain unassigned (e.g., all profiles exhausted), append them to last profile fallback
            if ($idx < $n) {
                // find any profile with capacity or fallback to first active
                $assigned = false;
                foreach ($profilesMeta as $j => $pmf) {
                    if ($pmf['max_sends'] === 0 || $pmf['max_sends'] > $pmf['used']) {
                        $parts[$j] = array_merge($parts[$j], array_slice($allRecipients, $idx));
                        $assigned = true; break;
                    }
                }
                if (!$assigned) {
                    $parts[0] = array_merge($parts[0], array_slice($allRecipients, $idx));
                }
            }

            // Now spawn per-profile workers / or send in-process. For each profile, determine effective send_rate (override)
            $spawnFailures = [];
            if (function_exists('fastcgi_finish_request')) {
                // detach response then do sends with workers if configured
                header("Location: $redirect");
                echo "<!doctype html><html><body>Sending in background... Redirecting.</body></html>";
                @ob_end_flush();
                @flush();
                fastcgi_finish_request();

                ignore_user_abort(true);
                set_time_limit(0);

                foreach ($profilesMeta as $idx => $pm) {
                    $p = $pm['profile'];
                    $recips = array_values(array_unique(array_map('trim', $parts[$idx])));
                    if (empty($recips)) continue;
                    $recipsText = implode("\n", $recips);
                    $effectiveRate = $pm['send_rate'] > 0 ? $pm['send_rate'] : $defaultPerProfilePerSec;
                    $overrides = ['send_rate' => $effectiveRate];
                    try {
                        // Use concurrent sending if workers configured and pcntl available
                        if ($globalWorkers > 1 && function_exists('pcntl_fork')) {
                            send_campaign_with_connections_for_profile_concurrent($pdo, $campaign, $recips, $p, false);
                        } else {
                            send_campaign_real($pdo, $campaign, $recipsText, false, (int)$p['id'], $overrides);
                        }
                    } catch (Exception $e) {
                        // continue
                    }
                }

                // CRITICAL: Close database connection before exit to prevent "too many connections"
                $pdo = null;
                exit;
            }

            // Otherwise attempt to spawn parallel workers per profile (passing overrides so each worker can throttle)
            foreach ($profilesMeta as $idx => $pm) {
                $p = $pm['profile'];
                $recips = array_values(array_unique(array_map('trim', $parts[$idx])));
                if (empty($recips)) continue;
                $effectiveRate = $pm['send_rate'] > 0 ? $pm['send_rate'] : $defaultPerProfilePerSec;
                $overrides = ['send_rate' => $effectiveRate];
                $cycleDelay = (int)($p['cycle_delay_ms'] ?? DEFAULT_CYCLE_DELAY_MS);
                
                // Spawn parallel workers for this profile's recipients
                $result = spawn_parallel_workers($pdo, (int)$campaign['id'], $recips, $globalWorkers, $globalMessagesPerWorker, (int)$p['id'], $overrides, $cycleDelay);
                
                if (!$result['success']) {
                    // Add failures for synchronous fallback
                    foreach ($result['failures'] as $failure) {
                        $recipsText = implode("\n", $failure['chunk']);
                        $spawnFailures[] = ['profile' => $p, 'recips' => $recipsText, 'overrides' => $overrides];
                    }
                }
            }

            // If any spawn failed, fallback to synchronous sending for those profiles (guarantees actual send)
            if (!empty($spawnFailures)) {
                // send redirect first so UI isn't waiting too long
                header("Location: $redirect");
                echo "<!doctype html><html><body>Sending (fallback) in progress... Please wait if your browser does not redirect.</body></html>";
                @ob_end_flush();
                @flush();

                ignore_user_abort(true);
                set_time_limit(0);

                foreach ($spawnFailures as $fail) {
                    try {
                        send_campaign_real($pdo, $campaign, $fail['recips'], false, (int)$fail['profile']['id'], $fail['overrides']);
                    } catch (Exception $e) {
                        // continue
                    }
                }

                // CRITICAL: Close database connection before exit to prevent "too many connections"
                $pdo = null;
                exit;
            }

            // All spawns succeeded -> safe to redirect immediately
            header("Location: $redirect");
            exit;
        } else {
            // Rotation disabled: send entire campaign with the selected SMTP using parallel workers
            // Get the profile for this campaign (either forced or first active)
            $profile = null;
            if (!empty($activeProfiles)) {
                // Try to find profile matching campaign's from_email
                foreach ($activeProfiles as $p) {
                    if (strtolower($p['from_email']) === strtolower($campaign['from_email'])) {
                        $profile = $p;
                        break;
                    }
                }
                // Fallback to first active profile
                if (!$profile) $profile = $activeProfiles[0];
            }
            
            $workers = DEFAULT_WORKERS;
            $messagesPerWorker = DEFAULT_MESSAGES_PER_WORKER;
            
            if ($profile) {
                // Enforce MAX_WORKERS cap on profile settings to prevent database connection exhaustion
                $workers = max(MIN_WORKERS, min(MAX_WORKERS, (int)($profile['workers'] ?? DEFAULT_WORKERS)));
                
                // Log warning for large worker counts
                if ($workers >= WORKERS_WARNING_THRESHOLD) {
                    error_log("Warning: Profile {$profile['id']} requesting $workers workers for campaign " . (int)$campaign['id'] . " (capped at MAX_WORKERS=" . MAX_WORKERS . ")");
                }
                
                $messagesPerWorker = max(MIN_MESSAGES_PER_WORKER, (int)($profile['messages_per_worker'] ?? DEFAULT_MESSAGES_PER_WORKER));
            }
            
            if (function_exists('fastcgi_finish_request')) {
                header("Location: $redirect");
                echo "<!doctype html><html><body>Sending in background... Redirecting.</body></html>";
                @ob_end_flush();
                @flush();
                fastcgi_finish_request();

                ignore_user_abort(true);
                set_time_limit(0);

                try {
                    // Use concurrent sending if workers > 1 and profile is configured
                    if ($workers > 1 && $profile && function_exists('pcntl_fork')) {
                        send_campaign_with_connections_for_profile_concurrent($pdo, $campaign, $allRecipients, $profile, false);
                    } else {
                        send_campaign_real($pdo, $campaign, $recipientsTextFull);
                    }
                } catch (Exception $e) {
                } finally {
                    // CRITICAL: Close database connection before exit to prevent "too many connections"
                    $pdo = null;
                }
                exit;
            }

            // Try to spawn parallel workers
            $cycleDelay = $profile ? (int)($profile['cycle_delay_ms'] ?? DEFAULT_CYCLE_DELAY_MS) : DEFAULT_CYCLE_DELAY_MS;
            $result = spawn_parallel_workers($pdo, (int)$campaign['id'], $allRecipients, $workers, $messagesPerWorker, $profile ? (int)$profile['id'] : null, [], $cycleDelay);
            if (!$result['success'] || $result['workers'] === 0) {
                // synchronous fallback (web request will wait) to ensure delivery instead of remaining 'sending'
                header("Location: $redirect");
                echo "<!doctype html><html><body>Sending (fallback) in progress... Please wait if your browser does not redirect.</body></html>";
                @ob_end_flush();
                @flush();
                
                ignore_user_abort(true);
                set_time_limit(0);
                try {
                    send_campaign_real($pdo, $campaign, $recipientsTextFull);
                } catch (Exception $e) {
                } finally {
                    // CRITICAL: Close database connection before exit to prevent "too many connections"
                    $pdo = null;
                }
                exit;
            }

            // All workers spawned successfully
            header("Location: $redirect");
            exit;
        }
        */
        // END OLD COMPLEX LOGIC

    }
    header("Location: $redirect");
    exit;
}

if ($action === 'save_rotation') {
    $rotation_enabled      = isset($_POST['rotation_enabled']) ? 1 : 0;
    
    // Read workers and messages_per_worker from POST with validation - no maximum limit
    $workers               = max(MIN_WORKERS, (int)($_POST['workers'] ?? DEFAULT_WORKERS));
    
    // Log warning for large worker counts
    if ($workers >= WORKERS_WARNING_THRESHOLD) {
        error_log("Warning: Rotation settings being saved with $workers workers");
    }
    
    $messages_per_worker   = max(MIN_MESSAGES_PER_WORKER, min(MAX_MESSAGES_PER_WORKER, (int)($_POST['messages_per_worker'] ?? DEFAULT_MESSAGES_PER_WORKER)));
    
    // Keep default values for backward compatibility (not shown in UI)
    $rotation_mode         = 'sequential';
    $batch_size            = 1;
    $max_sends_per_profile = 0;

    update_rotation_settings($pdo, [
        'rotation_enabled'      => $rotation_enabled,
        'mode'                  => $rotation_mode,
        'batch_size'            => $batch_size,
        'max_sends_per_profile' => $max_sends_per_profile,
        'workers'               => $workers,
        'messages_per_worker'   => $messages_per_worker,
    ]);

    header("Location: ?page=list&rot_saved=1");
    exit;
}

if ($action === 'save_profile') {
    $profile_id   = (int)($_POST['profile_id'] ?? 0);
    $profile_name = trim($_POST['profile_name'] ?? '');
    $type         = $_POST['type'] === 'api' ? 'api' : 'smtp';
    $from_email   = trim($_POST['from_email'] ?? '');
    $provider     = trim($_POST['provider'] ?? '');
    
    // Handle host selection: prioritize custom_host, then dropdown selection
    $custom_host  = trim($_POST['custom_host'] ?? '');
    if ($custom_host !== '') {
        $host = $custom_host;
    } else {
        $host = trim($_POST['host'] ?? '');
    }
    
    $port         = (int)($_POST['port'] ?? 0);
    $username     = trim($_POST['username'] ?? '');
    $password     = trim($_POST['password'] ?? '');
    $api_key      = trim($_POST['api_key'] ?? '');
    $api_url      = trim($_POST['api_url'] ?? '');
    $headers_json = trim($_POST['headers_json'] ?? '');
    $active       = isset($_POST['active']) ? 1 : 0;

    $profile_sender_name = trim($_POST['sender_name'] ?? '');
    $profile_reply_to = trim($_POST['reply_to'] ?? '');

    $bounce_server = trim($_POST['bounce_imap_server'] ?? '');
    $bounce_user = trim($_POST['bounce_imap_user'] ?? '');
    $bounce_pass = trim($_POST['bounce_imap_pass'] ?? '');

    // Proxy settings
    $proxy_enabled = isset($_POST['proxy_enabled']) ? 1 : 0;
    $proxy_type    = in_array(trim($_POST['proxy_type'] ?? 'http'), ['http', 'socks4', 'socks5'], true) ? trim($_POST['proxy_type']) : 'http';
    $proxy_host    = trim($_POST['proxy_host'] ?? '');
    $proxy_port    = max(1, min(65535, (int)($_POST['proxy_port'] ?? 8080)));
    $proxy_user    = trim($_POST['proxy_user'] ?? '');
    $proxy_pass    = (string)($_POST['proxy_pass'] ?? '');
    
    // Workers field - controls number of parallel workers - no maximum limit
    $profile_workers = max(MIN_WORKERS, (int)($_POST['workers'] ?? DEFAULT_WORKERS));
    
    // Log warning for large worker counts
    if ($profile_workers >= WORKERS_WARNING_THRESHOLD) {
        error_log("Warning: Sending profile being saved with $profile_workers workers");
    }
    
    // Messages per worker field - controls maximum number of messages each worker handles
    $profile_messages_per_worker = max(MIN_MESSAGES_PER_WORKER, min(MAX_MESSAGES_PER_WORKER, (int)($_POST['messages_per_worker'] ?? DEFAULT_MESSAGES_PER_WORKER)));
    
    // Cycle delay in milliseconds - delay between worker processing cycles
    $profile_cycle_delay_ms = max(MIN_CYCLE_DELAY_MS, min(MAX_CYCLE_DELAY_MS, (int)($_POST['cycle_delay_ms'] ?? DEFAULT_CYCLE_DELAY_MS)));
    
    // Keep old fields for backward compatibility (but set defaults, not from form)
    $random_en    = 0;
    $max_sends    = 0;
    $send_rate    = 0;

    try {
        if ($profile_id > 0) {
            $stmt = $pdo->prepare("
                UPDATE sending_profiles SET
                  profile_name=?, type=?, from_email=?, provider=?, host=?, port=?, username=?, password=?,
                  api_key=?, api_url=?, headers_json=?, active=?,
                  bounce_imap_server=?, bounce_imap_user=?, bounce_imap_pass=?, sender_name=?, reply_to=?, workers=?, messages_per_worker=?, cycle_delay_ms=?,
                  proxy_enabled=?, proxy_type=?, proxy_host=?, proxy_port=?, proxy_user=?, proxy_pass=?
                WHERE id=?
            ");
            $stmt->execute([
                $profile_name, $type, $from_email, $provider, $host, $port, $username, $password,
                $api_key, $api_url, $headers_json, $active,
                $bounce_server, $bounce_user, $bounce_pass, $profile_sender_name, $profile_reply_to, $profile_workers, $profile_messages_per_worker, $profile_cycle_delay_ms,
                $proxy_enabled, $proxy_type, $proxy_host, $proxy_port, $proxy_user, $proxy_pass,
                $profile_id
            ]);
        } else {
            $stmt = $pdo->prepare("
                INSERT INTO sending_profiles
                  (profile_name, type, from_email, provider, host, port, username, password,
                   api_key, api_url, headers_json, active,
                   bounce_imap_server, bounce_imap_user, bounce_imap_pass, sender_name, reply_to, workers, messages_per_worker, cycle_delay_ms,
                   proxy_enabled, proxy_type, proxy_host, proxy_port, proxy_user, proxy_pass)
                VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
            ");
            $stmt->execute([
                $profile_name, $type, $from_email, $provider, $host, $port, $username, $password,
                $api_key, $api_url, $headers_json, $active,
                $bounce_server, $bounce_user, $bounce_pass, $profile_sender_name, $profile_reply_to, $profile_workers, $profile_messages_per_worker, $profile_cycle_delay_ms,
                $proxy_enabled, $proxy_type, $proxy_host, $proxy_port, $proxy_user, $proxy_pass
            ]);
        }
    } catch (Exception $e) {
        // Fallback for tables without newer columns
        try {
            if ($profile_id > 0) {
                $stmt = $pdo->prepare("
                    UPDATE sending_profiles SET
                      profile_name=?, type=?, from_email=?, host=?, port=?, username=?, password=?,
                      api_key=?, api_url=?, headers_json=?, active=?, sender_name=?, reply_to=?, workers=?, messages_per_worker=?
                    WHERE id=?
                ");
                $stmt->execute([
                    $profile_name, $type, $from_email, $host, $port, $username, $password,
                    $api_key, $api_url, $headers_json, $active, $profile_sender_name, $profile_reply_to, $profile_workers, $profile_messages_per_worker,
                    $profile_id
                ]);
            } else {
                $stmt = $pdo->prepare("
                    INSERT INTO sending_profiles
                      (profile_name, type, from_email, host, port, username, password,
                       api_key, api_url, headers_json, active, sender_name, reply_to, workers, messages_per_worker)
                    VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
                ");
                $stmt->execute([
                    $profile_name, $type, $from_email, $host, $port, $username, $password,
                    $api_key, $api_url, $headers_json, $active, $profile_sender_name, $profile_reply_to, $profile_workers, $profile_messages_per_worker
                ]);
            }
        } catch (Exception $e2) {
            // Final fallback without provider, workers, reply_to
            try {
                if ($profile_id > 0) {
                    $stmt = $pdo->prepare("
                        UPDATE sending_profiles SET
                          profile_name=?, type=?, from_email=?, host=?, port=?, username=?, password=?,
                          api_key=?, api_url=?, headers_json=?, active=?, sender_name=?
                        WHERE id=?
                    ");
                    $stmt->execute([
                        $profile_name, $type, $from_email, $host, $port, $username, $password,
                        $api_key, $api_url, $headers_json, $active, $profile_sender_name,
                        $profile_id
                    ]);
                } else {
                    $stmt = $pdo->prepare("
                        INSERT INTO sending_profiles
                          (profile_name, type, from_email, host, port, username, password,
                           api_key, api_url, headers_json, active, sender_name)
                        VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
                    ");
                    $stmt->execute([
                        $profile_name, $type, $from_email, $host, $port, $username, $password,
                        $api_key, $api_url, $headers_json, $active, $profile_sender_name
                    ]);
                }
            } catch (Exception $e3) {
                error_log("Failed to save profile: " . $e3->getMessage());
            }
        }
    }

    header("Location: ?page=list&profiles=1");
    exit;
}

if ($action === 'delete_profile') {
    $pid = (int)($_POST['profile_id'] ?? 0);
    if ($pid > 0) {
        $stmt = $pdo->prepare("DELETE FROM sending_profiles WHERE id = ?");
        $stmt->execute([$pid]);
    }
    header("Location: ?page=list&profiles=1");
    exit;
}

///////////////////////
//  CONTACTS ACTIONS
///////////////////////
if ($action === 'create_contact_list') {
    $listName = trim($_POST['list_name'] ?? '');
    if ($listName !== '') {
        $stmt = $pdo->prepare("INSERT INTO contact_lists (name, type) VALUES (?, 'list')");
        $stmt->execute([$listName]);
    }
    header("Location: ?page=contacts");
    exit;
}

if ($action === 'delete_contact_list') {
    $lid = (int)($_POST['list_id'] ?? 0);
    if ($lid > 0) {
        $stmt = $pdo->prepare("DELETE FROM contacts WHERE list_id = ?");
        $stmt->execute([$lid]);
        $stmt = $pdo->prepare("DELETE FROM contact_lists WHERE id = ?");
        $stmt->execute([$lid]);
    }
    header("Location: ?page=contacts");
    exit;
}

if ($action === 'add_contact_manual') {
    $lid    = (int)($_POST['list_id'] ?? 0);
    $email  = trim($_POST['email'] ?? '');
    $first  = trim($_POST['first_name'] ?? '');
    $last   = trim($_POST['last_name'] ?? '');
    if ($lid > 0 && filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $stmt = $pdo->prepare("INSERT INTO contacts (list_id, email, first_name, last_name) VALUES (?, ?, ?, ?)");
        $stmt->execute([$lid, strtolower($email), $first, $last]);
    }
    header("Location: ?page=contacts&list_id=".$lid);
    exit;
}

if ($action === 'upload_contacts_csv') {
    $lid = (int)($_POST['list_id'] ?? 0);
    if ($lid > 0 && isset($_FILES['csv_files'])) {
        $uploaded_files = $_FILES['csv_files'];
        $file_count = is_array($uploaded_files['tmp_name']) ? count($uploaded_files['tmp_name']) : 1;
        
        // Handle multiple files
        for ($i = 0; $i < $file_count; $i++) {
            $tmp_name = is_array($uploaded_files['tmp_name']) ? $uploaded_files['tmp_name'][$i] : $uploaded_files['tmp_name'];
            $error = is_array($uploaded_files['error']) ? $uploaded_files['error'][$i] : $uploaded_files['error'];
            
            if ($error === UPLOAD_ERR_OK && is_uploaded_file($tmp_name)) {
                $path = $tmp_name;
                if (($fh = fopen($path, 'r')) !== false) {
                    $header = fgetcsv($fh);
                    $rows   = [];
                    $emailCol = 0;

                    if ($header !== false) {
                        $found = null;
                        foreach ($header as $j => $col) {
                            if (stripos($col, 'email') !== false) {
                                $found = $j;
                                break;
                            }
                        }
                        if ($found === null) {
                            $emailCol = 0;
                            $rows[] = $header;
                        } else {
                            $emailCol = $found;
                        }

                        while (($row = fgetcsv($fh)) !== false) {
                            $rows[] = $row;
                        }
                    }
                    fclose($fh);

                    $ins = $pdo->prepare("INSERT INTO contacts (list_id, email) VALUES (?, ?) ON DUPLICATE KEY UPDATE email=email");
                    foreach ($rows as $row) {
                        if (!isset($row[$emailCol])) continue;
                        $email = trim($row[$emailCol]);
                        if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) {
                            if (is_unsubscribed($pdo, $email)) continue;
                            try {
                                $ins->execute([$lid, strtolower($email)]);
                            } catch (Exception $e) {
                                // Skip duplicate emails
                            }
                        }
                    }
                }
            }
        }
    }
    header("Location: ?page=contacts&list_id=".$lid);
    exit;
}

///////////////////////
//  DATA FOR PAGES
///////////////////////
// Check if PDO is available before querying database
if ($pdo === null) {
    // PDO not initialized - database not configured yet
    // Redirect to installation if not already there
    if (!isset($_GET['action']) || !in_array($_GET['action'], ['install', 'do_install'])) {
        header('Location: ' . $_SERVER['SCRIPT_NAME'] . '?action=install');
        exit;
    }
    // Set empty defaults for installation page
    $rotationSettings = ['rotation_enabled' => 0];
    $profiles = [];
    $contactLists = [];
} else {
    // PDO is available, fetch data
    $rotationSettings = get_rotation_settings($pdo);
    $profiles         = get_profiles($pdo);
    $contactLists     = get_contact_lists($pdo);
}

$editProfile      = null;
if ($page === 'list' && isset($_GET['edit_profile'])) {
    $eid = (int)$_GET['edit_profile'];
    foreach ($profiles as $p) {
        if ((int)$p['id'] === $eid) {
            $editProfile = $p;
            break;
        }
    }
}

$isSingleSendsPage = in_array($page, ['list','editor','review','stats'], true);
$isContactsPage = ($page === 'contacts');
$isDashboardPage = ($page === 'dashboard');


?>
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>MAFIA MAILER - Professional Email Marketing</title>
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
  <!-- Chart.js without SRI - consider hosting locally for production -->
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
  <style>
    :root {
      --sg-blue: #1A82E2;
      --sg-blue-dark: #0F5BB5;
      --sg-blue-light: #E3F2FD;
      --sg-bg: #F6F8FB;
      --sg-border: #E3E8F0;
      --sg-text: #1F2933;
      --sg-muted: #6B778C;
      --sg-success: #12B886;
      --sg-success-light: #D3F9E9;
      --sg-danger: #FA5252;
      --sg-danger-light: #FFE5E5;
      --sg-warning: #FAB005;
      --sg-warning-light: #FFF4DB;
      --sg-shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
      --sg-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
      --sg-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
    }
    * {
      box-sizing: border-box;
    }
    html {
      scroll-behavior: smooth;
    }
    body {
      margin: 0;
      font-family: "Open Sans", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      background: var(--sg-bg);
      color: var(--sg-text);
      line-height: 1.6;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }
    a { 
      color: var(--sg-blue-dark); 
      text-decoration: none;
      transition: color 0.15s ease;
    }
    a:hover { 
      text-decoration: underline;
      color: var(--sg-blue);
    }

    .app-shell {
      display: flex;
      min-height: 100vh;
      overflow: hidden;
    }

    /* MAIN LEFT NAV (SendGrid-style) */
    .nav-main {
      width: 260px;
      background: #ffffff;
      border-right: 1px solid var(--sg-border);
      display: flex;
      flex-direction: column;
      position: relative;
      z-index: 200;
    }
    .nav-header {
      padding: 20px 16px 16px;
      border-bottom: 1px solid var(--sg-border);
      display: flex;
      align-items: center;
      gap: 12px;
    }
    .nav-avatar {
      width: 40px;
      height: 40px;
      border-radius: 999px;
      background: linear-gradient(135deg, var(--sg-blue) 0%, #0D6EFD 100%);
      color: #fff;
      display: flex;
      align-items: center;
      justify-content: center;
      font-weight: 700;
      font-size: 16px;
      box-shadow: 0 2px 8px rgba(26, 130, 226, 0.25);
    }
    .nav-user-info {
      display: flex;
      flex-direction: column;
      gap: 2px;
    }
    .nav-user-email {
      font-size: 14px;
      font-weight: 600;
      color: var(--sg-text);
    }
    .nav-user-sub {
      font-size: 12px;
      color: var(--sg-muted);
      font-weight: 500;
    }
    .nav-scroll {
      flex: 1;
      overflow-y: auto;
      padding: 12px 10px 16px;
    }
    .nav-section-title {
      font-size: 11px;
      font-weight: 700;
      text-transform: uppercase;
      color: var(--sg-muted);
      letter-spacing: .08em;
      margin: 14px 10px 6px;
    }
    .nav-link {
      display: flex;
      align-items: center;
      padding: 10px 14px;
      margin: 2px 4px;
      border-radius: 8px;
      font-size: 14px;
      color: var(--sg-text);
      transition: all 0.2s ease;
      font-weight: 500;
      text-decoration: none;
    }
    .nav-link span {
      flex: 1;
    }
    .nav-link.active {
      background: linear-gradient(135deg, var(--sg-blue-light) 0%, #E9F3FF 100%);
      color: var(--sg-blue-dark);
      font-weight: 600;
      box-shadow: var(--sg-shadow-sm);
    }
    .nav-link:hover {
      background: var(--sg-bg);
      text-decoration: none;
      color: var(--sg-blue);
      transform: translateX(2px);
    }
    .nav-link.active:hover {
      transform: translateX(0);
    }
    .nav-footer {
      padding: 12px 14px;
      border-top: 1px solid var(--sg-border);
      font-size: 11px;
      color: var(--sg-muted);
      line-height: 1.6;
    }

    /* Mobile menu toggle button */
    .mobile-menu-toggle {
      display: none;
      position: fixed;
      top: 16px;
      left: 16px;
      z-index: 300;
      width: 44px;
      height: 44px;
      background: var(--sg-blue);
      color: white;
      border: none;
      border-radius: 8px;
      cursor: pointer;
      align-items: center;
      justify-content: center;
      box-shadow: 0 2px 8px rgba(0,0,0,0.15);
      transition: all 0.2s ease;
    }
    .mobile-menu-toggle:hover {
      background: var(--sg-blue-dark);
      transform: scale(1.05);
    }
    .mobile-menu-toggle:active {
      transform: scale(0.95);
    }
    
    /* Mobile nav overlay */
    .nav-overlay {
      display: none;
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: rgba(0, 0, 0, 0.5);
      z-index: 199;
      opacity: 0;
      transition: opacity 0.3s ease;
    }
    .nav-overlay.active {
      opacity: 1;
    }

    /* Sending profiles slide panel */
    .sidebar-sp {
      position: relative;
      width: 0;
      flex-shrink: 0;
      transition: width 0.25s ease;
      overflow: hidden;
      pointer-events: none;
    }
    .sidebar-sp.open {
      width: 400px;
      pointer-events: auto;
    }
    .sidebar-inner {
      position: fixed;
      left: 260px;
      top: 0;
      bottom: 0;
      width: 400px;
      background: #fff;
      border-right: 1px solid var(--sg-border);
      box-shadow: 4px 0 16px rgba(15, 91, 181, 0.12);
      display: flex;
      flex-direction: column;
      transform: translateX(-100%);
      transition: transform 0.25s ease, visibility 0.2s ease;
      z-index: 30;
      visibility: hidden;
      pointer-events: none;
    }
    .sidebar-sp.open .sidebar-inner {
      transform: translateX(0);
      visibility: visible;
      pointer-events: auto;
    }

    .page-wrapper {
      flex: 1;
      transition: transform 0.25s ease;
      display: flex;
      flex-direction: column;
    }
    .page-wrapper.shifted {
      transform: translateX(400px);
    }

    .topbar {
      height: 64px;
      background: #fff;
      border-bottom: 1px solid var(--sg-border);
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 0 32px;
      box-shadow: var(--sg-shadow-sm);
      /* Sticky positioning keeps topbar visible while scrolling - intentional for better navigation */
      position: sticky;
      top: 0;
      z-index: 100;
    }
    .topbar .brand {
      font-weight: 700;
      font-size: 18px;
      display: flex;
      align-items: center;
      gap: 10px;
      color: var(--sg-text);
    }
    .topbar .brand-dot {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: linear-gradient(135deg, var(--sg-blue) 0%, var(--sg-blue-dark) 100%);
      box-shadow: 0 0 0 3px rgba(26,130,226,0.2);
      animation: pulse-dot 2s ease-in-out infinite;
    }
    @keyframes pulse-dot {
      0%, 100% {
        transform: scale(1);
      }
      50% {
        transform: scale(1.1);
      }
    }
    .topbar-actions {
      display: flex;
      align-items: center;
      gap: 8px;
    }
    .btn {
      border-radius: 6px;
      border: 1px solid transparent;
      padding: 8px 16px;
      font-size: 14px;
      font-weight: 500;
      cursor: pointer;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 6px;
      background: #fff;
      color: var(--sg-text);
      transition: all 0.2s ease;
      text-decoration: none;
    }
    .btn:hover {
      text-decoration: none;
      transform: translateY(-1px);
      box-shadow: var(--sg-shadow-sm);
    }
    .btn-primary {
      background: var(--sg-blue);
      color: #fff;
      border-color: var(--sg-blue);
      box-shadow: var(--sg-shadow-sm);
    }
    .btn-primary:hover {
      background: var(--sg-blue-dark);
      border-color: var(--sg-blue-dark);
      box-shadow: var(--sg-shadow-md);
    }
    .btn-outline {
      background: #fff;
      border-color: var(--sg-border);
      color: var(--sg-text);
    }
    .btn-outline:hover {
      border-color: var(--sg-blue);
      color: var(--sg-blue);
      background: var(--sg-blue-light);
    }
    .btn-danger {
      background: var(--sg-danger);
      border-color: var(--sg-danger);
      color: #fff;
      box-shadow: var(--sg-shadow-sm);
    }
    .btn-danger:hover {
      background: #E03131;
      border-color: #E03131;
      box-shadow: var(--sg-shadow-md);
    }

    .main-content {
      padding: 32px 40px 40px;
      flex: 1;
      overflow-y: auto;
      max-width: 1400px;
      margin: 0 auto;
      width: 100%;
    }

    .page-title {
      font-size: 28px;
      font-weight: 700;
      margin-bottom: 8px;
      color: var(--sg-text);
      letter-spacing: -0.02em;
    }
    .page-subtitle {
      font-size: 14px;
      color: var(--sg-muted);
      margin-bottom: 24px;
      line-height: 1.5;
    }

    .card {
      background: #fff;
      border-radius: 8px;
      border: 1px solid var(--sg-border);
      padding: 20px 24px;
      margin-bottom: 20px;
      box-shadow: var(--sg-shadow-sm);
      transition: box-shadow 0.2s ease, transform 0.2s ease;
    }
    .card:hover {
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
    }
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 16px;
      padding-bottom: 12px;
      border-bottom: 2px solid var(--sg-border);
    }
    .card-title {
      font-weight: 600;
      font-size: 16px;
      color: var(--sg-text);
    }
    .card-subtitle {
      font-size: 13px;
      color: var(--sg-muted);
      margin-top: 4px;
      line-height: 1.4;
    }

    .table {
      width: 100%;
      border-collapse: collapse;
      font-size: 14px;
    }
    .table th, .table td {
      padding: 14px 12px;
      border-bottom: 1px solid var(--sg-border);
      text-align: left;
    }
    .table th {
      font-size: 12px;
      text-transform: uppercase;
      letter-spacing: 0.05em;
      color: var(--sg-muted);
      font-weight: 600;
      background-color: var(--sg-bg);
    }
    .table tbody tr {
      transition: background-color 0.15s ease;
    }
    .table tbody tr:hover {
      background-color: var(--sg-bg);
    }
    .table tbody tr:last-child td {
      border-bottom: none;
    }
    .status-dot {
      width: 10px;
      height: 10px;
      border-radius: 50%;
      display: inline-block;
      margin-right: 8px;
    }
    @keyframes pulse {
      0%, 100% {
        opacity: 1;
      }
      50% {
        opacity: .7;
      }
    }
    @keyframes spin {
      from { transform: rotate(0deg); }
      to { transform: rotate(360deg); }
    }
    .status-dot.status-draft { 
      background: #CED4DA; /* Grey */
    }
    .status-dot.status-queued { 
      background: #FD7E14; /* Orange */
      animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
    }
    .status-dot.status-sending { 
      background: #FCC419; /* Yellow */
      animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
    }
    .status-dot.status-sent { 
      background: var(--sg-success); /* Green */
    }

    .badge {
      display: inline-flex;
      align-items: center;
      padding: 4px 10px;
      border-radius: 999px;
      font-size: 12px;
      font-weight: 500;
      background: var(--sg-blue-light);
      color: var(--sg-blue-dark);
      border: 1px solid var(--sg-blue);
    }

    .form-row {
      display: flex;
      gap: 12px;
      margin-bottom: 12px;
    }
    .form-group {
      margin-bottom: 12px;
      flex: 1;
    }
    .form-group label {
      font-size: 12px;
      font-weight: 600;
      display: block;
      margin-bottom: 4px;
    }
    .form-group small {
      display: block;
      font-size: 11px;
      color: var(--sg-muted);
    }
    input[type="text"],
    input[type="email"],
    input[type="number"],
    input[type="password"],
    textarea,
    select {
      width: 100%;
      padding: 10px 12px;
      font-size: 14px;
      border-radius: 6px;
      border: 1px solid var(--sg-border);
      outline: none;
      transition: all 0.2s ease;
      background: #fff;
      color: var(--sg-text);
    }
    input:focus, textarea:focus, select:focus {
      border-color: var(--sg-blue);
      box-shadow: 0 0 0 3px rgba(26,130,226,0.1);
      background: #fff;
    }
    input:hover, textarea:hover, select:hover {
      border-color: var(--sg-blue);
    }
    textarea {
      min-height: 160px;
      resize: vertical;
      font-family: inherit;
    }
    input::placeholder,
    textarea::placeholder {
      color: var(--sg-muted);
    }

    .sidebar-header {
      padding: 14px 16px;
      border-bottom: 1px solid var(--sg-border);
      display: flex;
      align-items: center;
      justify-content: space-between;
    }
    .sidebar-header-title {
      font-weight: 600;
      font-size: 14px;
    }

    .sidebar-body {
      padding: 14px 16px;
      overflow-y: auto;
      flex: 1;
    }
    .sidebar-section-title {
      font-size: 12px;
      font-weight: 600;
      margin-bottom: 6px;
      color: var(--sg-muted);
      text-transform: uppercase;
      letter-spacing: .05em;
    }
    .sidebar-foot {
      padding: 10px 16px 14px;
      border-top: 1px solid var(--sg-border);
    }

    .sp-rotation-card {
      border-radius: 6px;
      border: 1px solid var(--sg-border);
      padding: 10px 10px 12px;
      background: var(--sg-bg);
      margin-bottom: 14px;
    }
    .checkbox-row {
      display: flex;
      align-items: center;
      gap: 8px;
      margin-bottom: 8px;
      font-size: 13px;
    }
    .radio-row {
      display: flex;
      gap: 12px;
      margin-bottom: 8px;
      font-size: 13px;
    }

    .profiles-list {
      display: flex;
      flex-direction: column;
      gap: 8px;
      margin-bottom: 12px;
    }
    .profile-card {
      border-radius: 8px;
      border: 1px solid var(--sg-border);
      padding: 14px 16px;
      font-size: 13px;
      background: #fff;
      transition: all 0.2s ease;
      box-shadow: var(--sg-shadow-sm);
    }
    .profile-card:hover {
      border-color: var(--sg-blue);
      box-shadow: var(--sg-shadow-md);
      transform: translateY(-2px);
    }
    .profile-card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 4px;
    }
    .profile-name {
      font-weight: 600;
      font-size: 13px;
    }
    .profile-meta {
      font-size: 11px;
      color: var(--sg-muted);
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
    }
    .profile-actions {
      display: flex;
      gap: 6px;
      margin-top: 4px;
    }
    .btn-mini {
      font-size: 11px;
      padding: 2px 6px;
      border-radius: 4px;
      border: 1px solid var(--sg-border);
      background: #fff;
      cursor: pointer;
    }

    .sp-form {
      border-radius: 6px;
      border: 1px solid var(--sg-border);
      padding: 10px 10px 12px;
      margin-bottom: 10px;
    }
    .hint {
      font-size: 11px;
      color: var(--sg-muted);
    }
    .pill {
      display: inline-flex;
      align-items: center;
      padding: 2px 6px;
      border-radius: 999px;
      font-size: 11px;
      background: #EFF3F9;
      color: #4B5563;
    }

    /* Header stats (new simplified look) */
    .header-stats {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
      gap: 20px;
      background: #fff;
      border: 1px solid var(--sg-border);
      border-radius: 8px;
      padding: 24px;
      margin-bottom: 24px;
      box-shadow: var(--sg-shadow-sm);
    }
    .stat-item {
      text-align: center;
      padding: 12px;
      border-radius: 6px;
      transition: background-color 0.2s ease, transform 0.2s ease;
    }
    .stat-item:hover {
      background-color: var(--sg-bg);
      transform: translateY(-2px);
    }
    .stat-item .stat-num {
      font-size: 32px;
      font-weight: 700;
      color: var(--sg-blue-dark);
      line-height: 1;
      margin-bottom: 8px;
    }
    .stat-item .stat-label {
      font-size: 12px;
      color: var(--sg-muted);
      text-transform: uppercase;
      letter-spacing: .05em;
      font-weight: 600;
    }
    
    /* Enhanced download button styling in stats */
    .stat-item .btn-mini {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 4px 8px;
      font-size: 10px;
      border-radius: 4px;
      transition: all 0.2s ease;
    }
    .stat-item .btn-mini:hover {
      transform: translateY(-1px);
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    .stats-grid {
      display: grid;
      grid-template-columns: repeat(4, minmax(0, 1fr));
      gap: 12px;
      margin-bottom: 12px;
    }
    .stat-box {
      background: #fff;
      border-radius: 6px;
      border: 1px solid var(--sg-border);
      padding: 10px 12px;
    }
    .stat-label {
      font-size: 11px;
      text-transform: uppercase;
      color: var(--sg-muted);
      margin-bottom: 4px;
    }
    .stat-value {
      font-size: 18px;
      font-weight: 600;
    }
    .stat-sub {
      font-size: 11px;
      color: var(--sg-muted);
    }

    /* Contacts modal */
    .modal-backdrop {
      position: fixed;
      inset: 0;
      background: rgba(15,23,42,0.5);
      display: flex;
      justify-content: flex-end;
      z-index: 40;
      animation: fadeIn 0.2s ease;
    }
    @supports (backdrop-filter: blur(4px)) {
      .modal-backdrop {
        backdrop-filter: blur(4px);
      }
    }
    @keyframes fadeIn {
      from {
        opacity: 0;
      }
      to {
        opacity: 1;
      }
    }
    .modal-panel {
      width: 480px;
      max-width: 100%;
      background: #fff;
      padding: 28px 32px;
      box-shadow: var(--sg-shadow-lg);
      display: flex;
      flex-direction: column;
      animation: slideInRight 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }
    @keyframes slideInRight {
      from {
        transform: translateX(100%);
      }
      to {
        transform: translateX(0);
      }
    }

    /* HTML Editor modal (fixed center) */
    .html-editor-backdrop {
      display: none; /* hidden initially */
      position: fixed;
      inset: 0;
      background: rgba(15,23,42,0.6);
      display: none;
      align-items: center;
      justify-content: center;
      z-index: 80;
      animation: fadeIn 0.2s ease;
    }
    @supports (backdrop-filter: blur(4px)) {
      .html-editor-backdrop {
        backdrop-filter: blur(4px);
      }
    }
    .html-editor-backdrop.show {
      display: flex;
    }
    .html-editor-panel {
      width: 900px;
      max-width: calc(100% - 40px);
      max-height: 85vh;
      overflow: auto;
      background: #fff;
      padding: 24px;
      border-radius: 12px;
      box-shadow: var(--sg-shadow-lg);
      display: flex;
      flex-direction: column;
      gap: 12px;
      animation: scaleIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }
    @keyframes scaleIn {
      from {
        transform: scale(0.95);
        opacity: 0;
      }
      to {
        transform: scale(1);
        opacity: 1;
      }
    }
    .html-editor-panel textarea {
      min-height: 420px;
      width: 100%;
      padding: 12px;
      font-family: monospace;
      font-size: 13px;
      border: 1px solid #e6edf3;
      border-radius: 6px;
      resize: vertical;
    }
    .html-editor-actions {
      display: flex;
      gap: 8px;
      justify-content: flex-end;
    }

    .toast {
      position: fixed;
      right: 24px;
      top: 24px;
      z-index: 200;
      padding: 14px 18px;
      border-radius: 8px;
      color: #fff;
      font-weight: 600;
      font-size: 14px;
      box-shadow: var(--sg-shadow-lg);
      display: none;
      opacity: 0;
      transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
      transform: translateY(-10px);
    }
    .toast.show { 
      display: block; 
      opacity: 1;
      transform: translateY(0);
    }
    .toast.success { 
      background: var(--sg-success);
      border: 1px solid var(--sg-success);
    }
    .toast.error { 
      background: var(--sg-danger);
      border: 1px solid var(--sg-danger);
    }

    /* ========== REVIEW PAGE ENHANCEMENTS ========== */
    /* Make the Review page resemble SendGrid layout: left details, right preview (wider preview) */
    
    /* Status indicators for concurrent mode */
    .concurrent-badge {
      display: inline-block;
      padding: 2px 6px;
      font-size: 10px;
      font-weight: 600;
      background: #4CAF50;
      color: white;
      border-radius: 3px;
      margin-left: 8px;
      text-transform: uppercase;
    }
    .concurrent-badge.sequential {
      background: #9e9e9e;
    }
    
    .review-grid {
      display: grid;
      grid-template-columns: 1fr 680px; /* bigger preview panel */
      gap: 18px;
      align-items: start;
    }
    .review-summary {
      background: #fff;
      border-radius: 6px;
      border: 1px solid var(--sg-border);
      padding: 16px;
    }
    .review-row {
      display: flex;
      justify-content: space-between;
      padding: 10px 6px;
      border-bottom: 1px solid #f1f5f9;
    }
    .review-row .left { color: var(--sg-muted); font-weight:600; width:40%; }
    .review-row .right { width:58%; text-align:right; color:var(--sg-text); }

    .test-send-card {
      margin-top: 14px;
      background: #fff;
      border-radius: 6px;
      border: 1px solid var(--sg-border);
      padding: 14px;
    }
    .test-send-card small.hint { display:block; margin-top:8px; color:var(--sg-muted); }

    .preview-box {
      border:1px solid var(--sg-border);
      border-radius:6px;
      background:#fff;
      padding:10px;
      min-height:640px; /* taller preview */
      overflow:auto;
      transition: max-width 0.3s ease;
    }
    
    .preview-box[data-preview-mode="desktop"] {
      max-width: 100%;
    }
    
    .preview-box[data-preview-mode="mobile"] {
      max-width: 375px;
      margin: 0 auto;
    }
    
    .preview-box[data-preview-mode="plain"] {
      font-family: monospace;
      white-space: pre-wrap;
      background: #f8f9fa;
    }
    
    .preview-toggle-buttons {
      display: flex;
      gap: 4px;
      background: #f1f5f9;
      padding: 4px;
      border-radius: 6px;
    }
    
    .preview-toggle-btn {
      display: flex;
      align-items: center;
      gap: 4px;
      padding: 6px 12px;
      border: none;
      background: transparent;
      color: var(--sg-muted);
      font-size: 12px;
      font-weight: 600;
      cursor: pointer;
      border-radius: 4px;
      transition: all 0.2s;
    }
    
    .preview-toggle-btn:hover {
      background: rgba(255,255,255,0.5);
      color: var(--sg-text);
    }
    
    .preview-toggle-btn.active {
      background: #fff;
      color: var(--sg-primary);
      box-shadow: 0 1px 3px rgba(0,0,0,0.1);
    }
    
    .preview-toggle-btn svg {
      width: 14px;
      height: 14px;
    }
    
    /* Tag-based multi-select styles */
    .tag-select-container {
      position: relative;
    }
    
    .tag-select-display {
      min-height: 38px;
      border: 1px solid var(--sg-border);
      border-radius: 6px;
      padding: 4px 8px;
      background: #fff;
      cursor: pointer;
      display: flex;
      flex-wrap: wrap;
      gap: 4px;
      align-items: center;
      transition: all 0.2s ease;
    }
    
    .tag-select-display:hover {
      border-color: var(--sg-primary);
      box-shadow: 0 0 0 2px rgba(26, 130, 226, 0.08);
    }
    
    .tag-select-display.open {
      border-color: var(--sg-primary);
      box-shadow: 0 0 0 3px rgba(26, 130, 226, 0.12);
    }
    
    .tag-select-placeholder {
      color: var(--sg-muted);
      padding: 4px 6px;
      font-size: 14px;
    }
    
    .selected-tag {
      display: inline-flex;
      align-items: center;
      gap: 4px;
      background: linear-gradient(135deg, #1A82E2 0%, #1565C0 100%);
      color: white;
      padding: 3px 8px;
      border-radius: 12px;
      font-weight: 500;
      transition: all 0.2s ease;
      box-shadow: 0 1px 3px rgba(26, 130, 226, 0.3);
      white-space: nowrap;
    }
    
    /* Dynamic sizing based on number of tags */
    .tag-select-display[data-tag-count="1"] .selected-tag,
    .tag-select-display[data-tag-count="2"] .selected-tag {
      font-size: 13px;
      padding: 4px 10px;
    }
    
    .tag-select-display[data-tag-count="3"] .selected-tag,
    .tag-select-display[data-tag-count="4"] .selected-tag {
      font-size: 12px;
      padding: 3px 8px;
    }
    
    .tag-select-display[data-tag-count="5"] .selected-tag,
    .tag-select-display[data-tag-count="6"] .selected-tag {
      font-size: 11px;
      padding: 3px 7px;
    }
    
    .tag-select-display[data-tag-count="7"] .selected-tag,
    .tag-select-display[data-tag-count="8"] .selected-tag,
    .tag-select-display[data-tag-count="9"] .selected-tag,
    .tag-select-display[data-tag-count="10"] .selected-tag {
      font-size: 10px;
      padding: 2px 6px;
    }
    
    .selected-tag:hover {
      transform: translateY(-1px);
      box-shadow: 0 2px 6px rgba(26, 130, 226, 0.4);
    }
    
    .selected-tag-remove {
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      margin: 0;
      margin-left: 2px;
      background: rgba(255, 255, 255, 0.2);
      border: none;
      color: white;
      font-size: 14px;
      line-height: 1;
      width: 16px;
      height: 16px;
      border-radius: 50%;
      transition: all 0.15s ease;
    }
    
    .selected-tag-remove:hover {
      background: rgba(255, 255, 255, 0.3);
      transform: scale(1.1);
    }
    
    .tag-select-dropdown {
      position: absolute;
      top: 100%;
      left: 0;
      right: 0;
      background: white;
      border: 1px solid var(--sg-border);
      border-radius: 8px;
      margin-top: 4px;
      max-height: 320px;
      overflow-y: auto;
      z-index: 1000;
      box-shadow: 0 8px 24px rgba(0,0,0,0.12);
      display: none;
    }
    
    .tag-select-dropdown.open {
      display: block;
      animation: dropdownSlide 0.2s ease;
    }
    
    @keyframes dropdownSlide {
      from {
        opacity: 0;
        transform: translateY(-8px);
      }
      to {
        opacity: 1;
        transform: translateY(0);
      }
    }
    
    .tag-select-option {
      padding: 12px 14px;
      cursor: pointer;
      transition: all 0.15s ease;
      border-bottom: 1px solid #f1f5f9;
    }
    
    .tag-select-option:last-child {
      border-bottom: none;
    }
    
    .tag-select-option:hover {
      background: #f8f9fa;
      padding-left: 18px;
    }
    
    .tag-select-option.selected {
      background: linear-gradient(90deg, #e3f2fd 0%, #f8f9fa 100%);
      color: var(--sg-primary);
      font-weight: 600;
      border-left: 3px solid var(--sg-primary);
    }
    
    .tag-select-option-name {
      font-weight: 600;
      margin-bottom: 3px;
      color: var(--sg-text);
    }
    
    .tag-select-option-count {
      font-size: 12px;
      color: var(--sg-muted);
      font-weight: 400;
    }
    
    .tag-select-option.selected .tag-select-option-name {
      color: var(--sg-primary);
    }

    /*
      ============================
      NEW / FIXED STYLES FOR EDITOR
      These styles make the left "modules" area show tiles (like Image 2)
      and style the canvas placeholder and blocks for a nicer visual layout.
      ============================
    */
    .editor-shell {
      display: flex;
      gap: 18px;
      align-items: flex-start;
    }
    .editor-left {
      width: 320px; /* make left sidebar a fixed column like SendGrid */
      min-width: 260px;
      max-width: 360px;
      background: transparent;
      padding: 12px;
    }

    /* Modules grid: tiles with icons */
    .modules-grid {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 12px;
      align-items: start;
      margin-bottom: 10px;
    }
    .module-tile {
      background: #fff;
      border: 1px solid var(--sg-border);
      border-radius: 6px;
      padding: 18px 12px;
      text-align: center;
      cursor: pointer;
      color: var(--sg-muted);
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 8px;
      min-height: 84px;
      transition: box-shadow .15s ease, border-color .15s ease, transform .08s ease;
    }
    .module-tile .icon {
      font-size: 22px;
      color: var(--sg-blue);
      line-height: 1;
    }
    .module-tile:hover {
      border-color: var(--sg-blue);
      box-shadow: 0 6px 16px rgba(26,130,226,0.08);
      color: var(--sg-text);
      transform: translateY(-2px);
    }
    .modules-pane .sidebar-section-title {
      margin-top: 6px;
      margin-bottom: 12px;
    }

    /* Canvas and placeholder */
    .editor-right {
      flex: 1;
      display: flex;
      flex-direction: column;
      gap: 12px;
      padding-left: 12px;
    }
    .canvas-area {
      border-radius: 6px;
      border: 1px solid var(--sg-border);
      background: #fff;
      min-height: 420px;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 30px;
      position: relative;
      overflow: auto;
    }
    .drag-placeholder {
      background: #FBFCFD;
      border: 1px dashed #EEF2F6;
      padding: 60px 40px;
      color: #9AA6B2;
      border-radius: 6px;
      width: 70%;
      text-align: center;
      font-size: 15px;
    }

    /* Canvas block wrapper */
    .canvas-block {
      background: #fff;
      border: 1px solid #f1f5f9;
      border-radius: 6px;
      padding: 12px;
      margin-bottom: 12px;
      width: 100%;
      position: relative;
      box-shadow: none;
    }
    .canvas-block + .canvas-block {
      margin-top: 12px;
    }
    .block-remove {
      position: absolute;
      top: 8px;
      right: 8px;
      background: #fff;
      border: 1px solid var(--sg-border);
      border-radius: 4px;
      width: 28px;
      height: 28px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      font-size: 14px;
      color: #666;
    }
    .block-remove:hover {
      background: #fff;
      border-color: var(--sg-blue);
      color: var(--sg-blue-dark);
    }
    .block-content {
      min-height: 40px;
    }

    /* Code module visual improvements */
    .code-module {
      border: 1px dashed #eef2f6;
      border-radius: 6px;
      overflow: hidden;
    }
    .code-module-header {
      background: #1f6f9f;
      color: #fff;
      padding: 8px 10px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .code-module-body {
      padding: 12px;
      background: #fff;
      color: #333;
    }
    .code-placeholder {
      color: #9aa6b2;
      font-style: normal;
    }

    /* Responsive design improvements */
    @media (max-width: 1400px) {
      .review-grid { grid-template-columns: 1fr 520px; }
      .main-content {
        padding: 28px 32px 32px;
      }
    }

    @media (max-width: 1200px) {
      .editor-left { width: 300px; height: auto; }
      .drag-placeholder { width: 100%; }
      .canvas-block, .drag-placeholder { width: 100%; }
      .modules-grid { grid-template-columns: repeat(1, minmax(0,1fr)); }
      .header-stats {
        grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
        gap: 16px;
        padding: 20px;
      }
    }

    @media (max-width: 900px) {
      .form-row {
        flex-direction: column;
      }
      .stats-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }
      .header-stats {
        grid-template-columns: repeat(2, 1fr);
        gap: 12px;
        padding: 16px;
      }
      .stat-item .stat-num {
        font-size: 24px;
      }
      
      /* Mobile navigation */
      .nav-main {
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        transform: translateX(-100%);
        transition: transform 0.3s ease;
        z-index: 200;
        box-shadow: 4px 0 16px rgba(0, 0, 0, 0.2);
      }
      .nav-main.mobile-open {
        transform: translateX(0);
      }
      .mobile-menu-toggle {
        display: flex;
      }
      .nav-overlay {
        display: block;
      }
      .page-wrapper {
        margin-left: 0 !important;
      }
      .sidebar-inner {
        left: 0;
        width: 100%;
      }
      .sidebar-sp.open {
        width: 100%;
      }
      .page-wrapper.shifted {
        transform: none;
      }
      .editor-shell {
        flex-direction: column;
      }
      .editor-left, .editor-right { width: 100%; }
      .review-grid { grid-template-columns: 1fr; }
      .preview-box { min-height:420px; }
      .main-content {
        padding: 20px 16px 24px;
      }
      .page-title {
        font-size: 24px;
      }
      .card {
        padding: 16px 18px;
      }
    }
    
    @media (max-width: 600px) {
      .header-stats {
        grid-template-columns: 1fr;
      }
      .card-header .btn {
        width: 100%;
        justify-content: center;
      }
      .card-header {
        flex-direction: column;
        align-items: flex-start;
        gap: 12px;
      }
      .page-title {
        font-size: 20px;
      }
    }
  </style>
</head>
<body>
<div class="app-shell">

  <!-- Mobile Menu Toggle Button -->
  <button class="mobile-menu-toggle" id="mobileMenuToggle" onclick="toggleMobileMenu()">
    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <line x1="3" y1="12" x2="21" y2="12"></line>
      <line x1="3" y1="6" x2="21" y2="6"></line>
      <line x1="3" y1="18" x2="21" y2="18"></line>
    </svg>
  </button>

  <!-- Mobile Nav Overlay -->
  <div class="nav-overlay" id="navOverlay" onclick="closeMobileMenu()"></div>

  <!-- LEFT NAV (SendGrid-style) -->
  <aside class="nav-main">
    <div class="nav-header">
      <div class="nav-avatar"><?php echo strtoupper(substr($_SESSION['admin_username'] ?? 'A', 0, 1)); ?></div>
      <div class="nav-user-info">
        <div class="nav-user-email"><?php echo htmlspecialchars($_SESSION['admin_username'] ?? 'Admin'); ?></div>
        <div class="nav-user-sub">MAFIA MAILER</div>
      </div>
    </div>
    <div class="nav-scroll">
      <div class="nav-section-title">Email Marketing</div>
      <a href="?page=dashboard" class="nav-link <?php echo $isDashboardPage ? 'active' : ''; ?>">
        <span>Dashboard</span>
      </a>

      <div class="nav-section-title">Marketing</div>
      <a href="?page=list" class="nav-link <?php echo $isSingleSendsPage ? 'active' : ''; ?>">
        <span>Single Sends</span>
      </a>
      <a href="?page=contacts" class="nav-link <?php echo $isContactsPage ? 'active' : ''; ?>">
        <span>Contacts</span>
      </a>
      <a href="#" class="nav-link"><span>Design Library</span></a>

      <div class="nav-section-title">Analytics</div>
      <a href="?page=stats" class="nav-link"><span>Stats</span></a>
      <a href="?page=activity" class="nav-link"><span>Activity</span></a>
      <a href="?page=tracking" class="nav-link"><span>Tracking Settings</span></a>
    </div>
    <div class="nav-footer">
      <a href="?action=logout" style="color: var(--sg-danger); display: block; padding: 8px 0; font-weight: 600;">🚪 Logout</a>
      MAFIA MAILER v1.0<br>Professional Email Marketing
    </div>
  </aside>

  <!-- LEFT SLIDE PANEL: Sending Profiles -->
  <div class="sidebar-sp" id="spSidebar">
    <div class="sidebar-inner">
      <div class="sidebar-header">
        <div class="sidebar-header-title">Sending Profiles</div>
        <button class="btn btn-outline" type="button" id="spCloseBtn">✕</button>
      </div>
      <div class="sidebar-body">
        <!-- Rotation settings -->
        <form method="post" class="sp-rotation-card">
          <input type="hidden" name="action" value="save_rotation">
          <div class="sidebar-section-title">Rotation Settings</div>
          <div class="checkbox-row">
            <input type="checkbox" name="rotation_enabled" id="rot_enabled" <?php if ($rotationSettings['rotation_enabled']) echo 'checked'; ?>>
            <label for="rot_enabled">Enable Global SMTP/API Rotation</label>
          </div>
          <div class="form-group">
            <label>Workers (Minimum Override)</label>
            <input type="number" name="workers" min="<?php echo MIN_WORKERS; ?>" value="<?php echo (int)($rotationSettings['workers'] ?? DEFAULT_WORKERS); ?>">
            <small class="hint">
              <strong>Auto-calculated:</strong> Workers are automatically calculated based on email count (16 workers per 1,000 emails for maximum speed). 
              This setting acts as a <strong>minimum override</strong> - the system will use whichever is higher.
              <br><br>
              <strong>Optimized Examples:</strong> 1,000 emails = 16 workers | 10,000 emails = 160 workers | 79,258 emails = 1,280 workers
            </small>
          </div>
          <div class="form-group">
            <label>Messages Per Worker</label>
            <input type="number" name="messages_per_worker" min="<?php echo MIN_MESSAGES_PER_WORKER; ?>" max="<?php echo MAX_MESSAGES_PER_WORKER; ?>" value="<?php echo (int)($rotationSettings['messages_per_worker'] ?? DEFAULT_MESSAGES_PER_WORKER); ?>">
            <small class="hint">Number of emails each worker processes (<?php echo MIN_MESSAGES_PER_WORKER; ?>-<?php echo MAX_MESSAGES_PER_WORKER; ?>, default: <?php echo DEFAULT_MESSAGES_PER_WORKER; ?>). Controls distribution granularity across workers.</small>
          </div>
          <div class="form-group">
            <small class="hint" style="display:block; margin-top:8px;">
              When enabled, campaigns will rotate through all active sending profiles. 
              Configure individual profiles with Workers to control parallel sending speed.
              <?php if (function_exists('pcntl_fork')): ?>
                <br><strong style="color:#4CAF50;">✓ Parallel Mode Available:</strong> Multiple workers will send in parallel for maximum speed.
              <?php else: ?>
                <br><strong style="color:#ff9800;">⚠ Sequential Mode:</strong> PHP pcntl extension not available. Workers will process sequentially.
              <?php endif; ?>
            </small>
          </div>
          <button class="btn btn-primary" type="submit">Save Rotation</button>
        </form>

        <!-- Profiles list -->
        <div class="profiles-list">
          <div class="sidebar-section-title">Profiles</div>

          <!-- IP Reputation Notice -->
          <div style="background:#fff8e1;border:1px solid #ffe082;border-radius:6px;padding:10px 12px;margin-bottom:12px;font-size:12px;line-height:1.6;">
            <strong style="color:#b45309;">⚠ IP Reputation (SpamCop / Razor2)</strong><br>
            Your server's outbound IP may be blacklisted in SpamCop or flagged by Razor2 if it has sent spam before.
            These rules add up to <strong>-4.4 points</strong> to your spam score.<br>
            <br>
            <strong>Check &amp; fix:</strong><br>
            &nbsp;• <a href="https://www.spamcop.net/bl.shtml?<?php echo urlencode($_SERVER['SERVER_ADDR'] ?? ''); ?>" target="_blank" rel="noopener" style="color:#0066cc;">Check your IP on SpamCop ↗</a><br>
            &nbsp;• <a href="https://www.spamcop.net/fom-serve/cache/297.html" target="_blank" rel="noopener" style="color:#0066cc;">Request SpamCop delisting ↗</a><br>
            &nbsp;• <a href="http://razor.sourceforge.net/docs/draft-ietf-razor-rfc.html#submit" target="_blank" rel="noopener" style="color:#0066cc;">Razor2 revoke submission ↗</a><br>
            &nbsp;• Enable the <strong>Proxy</strong> option in your profile below to route sends through a clean IP.
          </div>
          <?php if (empty($profiles)): ?>
            <div class="hint">No profiles yet. Create your first SMTP or API profile below.</div>
          <?php else: ?>
            <?php foreach ($profiles as $p): ?>
              <div class="profile-card" id="profile-card-<?php echo (int)$p['id']; ?>">
                <div class="profile-card-header">
                  <div class="profile-name"><?php echo h($p['profile_name']); ?></div>
                  <div class="pill"><?php echo strtoupper($p['type']); ?> · <?php echo h($p['provider'] ?? 'Custom'); ?></div>
                </div>
                <div class="profile-meta">
                  <span>From: <?php echo h($p['from_email']); ?></span>
                  <?php if (!empty($p['sender_name'])): ?>
                    <span>Sender: <?php echo h($p['sender_name']); ?></span>
                  <?php endif; ?>
                  <span>Status: <?php echo $p['active'] ? 'Active' : 'Disabled'; ?></span>
                  <?php if (!empty($p['workers'])): ?>
                    <span>Workers: <?php echo (int)$p['workers']; ?></span>
                  <?php endif; ?>
                  <?php if (!empty($p['messages_per_worker'])): ?>
                    <span>Msgs/Worker: <?php echo (int)$p['messages_per_worker']; ?></span>
                  <?php endif; ?>
                  <?php if (!empty($p['proxy_enabled'])): ?>
                    <span>🔒 Proxy: <?php echo h(strtoupper($p['proxy_type'] ?? 'HTTP')); ?> <?php echo h($p['proxy_host'] ?? ''); ?>:<?php echo (int)($p['proxy_port'] ?? 8080); ?></span>
                  <?php endif; ?>
                </div>
                <div class="profile-actions">
                  <a href="?page=list&edit_profile=<?php echo (int)$p['id']; ?>" class="btn-mini">Edit</a>
                  <button type="button" class="btn-mini check-conn-btn" data-pid="<?php echo (int)$p['id']; ?>">Check Connection</button>
                  <?php if (!empty($p['proxy_enabled'])): ?>
                  <button type="button" class="btn-mini check-proxy-btn" data-pid="<?php echo (int)$p['id']; ?>">Test Proxy</button>
                  <?php endif; ?>
                  <form method="post" style="display:inline;">
                    <input type="hidden" name="action" value="delete_profile">
                    <input type="hidden" name="profile_id" value="<?php echo (int)$p['id']; ?>">
                    <button type="submit" class="btn-mini" onclick="return confirm('Delete this profile?');">Delete</button>
                  </form>
                </div>
                <div style="margin-top:8px; font-size:12px; color:var(--sg-muted);" id="profile-conn-status-<?php echo (int)$p['id']; ?>"></div>
              </div>
            <?php endforeach; ?>
          <?php endif; ?>
        </div>

        <!-- Add / Edit profile -->
        <div class="sidebar-section-title">
          <?php echo $editProfile ? 'Edit Profile' : 'Add New Profile'; ?>
        </div>
        <form method="post" class="sp-form" id="profileForm">
          <input type="hidden" name="action" value="save_profile">
          <input type="hidden" name="profile_id" value="<?php echo $editProfile ? (int)$editProfile['id'] : 0; ?>">
          <div class="form-group">
            <label>Profile Name</label>
            <input type="text" name="profile_name" required value="<?php echo $editProfile ? h($editProfile['profile_name']) : ''; ?>">
          </div>
          <div class="form-group">
            <label>Profile Type</label>
            <select name="type" id="pf_type">
              <option value="smtp" <?php if(!$editProfile || $editProfile['type']==='smtp') echo 'selected'; ?>>SMTP</option>
              <option value="api"  <?php if($editProfile && $editProfile['type']==='api') echo 'selected'; ?>>API</option>
            </select>
          </div>
          <div class="form-group">
            <label>From Email</label>
            <input type="email" name="from_email" required value="<?php echo $editProfile ? h($editProfile['from_email']) : ''; ?>">
          </div>

          <!-- NEW: Sender name per profile -->
          <div class="form-group">
            <label>Sender Name (display name)</label>
            <input type="text" name="sender_name" placeholder="e.g. Acme Co." value="<?php echo $editProfile ? h($editProfile['sender_name'] ?? '') : ''; ?>">
            <small class="hint">This name will be used in the From header when rotation is enabled (or as fallback).</small>
          </div>

          <!-- NEW: Reply-To address per profile -->
          <div class="form-group">
            <label>Reply-To Email</label>
            <input type="email" name="reply_to" placeholder="e.g. support@example.com" value="<?php echo $editProfile ? h($editProfile['reply_to'] ?? '') : ''; ?>">
            <small class="hint">Email address for replies. Leave empty to use From Email.</small>
          </div>

          <div class="form-group">
            <label>Provider</label>
            <select name="provider" id="pf_provider">
              <?php
                $providers = ['Gmail','Outlook','Yahoo','Zoho','SendGrid SMTP','MailGun','MailJet','SparkPost','Amazon SES','SendGrid API','MailJet API','Custom'];
                $selectedProvider = $editProfile ? ($editProfile['provider'] ?? 'Custom') : '';
                foreach ($providers as $prov):
              ?>
                <option value="<?php echo h($prov); ?>" <?php if ($selectedProvider === $prov) echo 'selected'; ?>>
                  <?php echo h($prov); ?>
                </option>
              <?php endforeach; ?>
            </select>
          </div>

          <div id="pf_smtp_fields" style="<?php echo (!$editProfile || $editProfile['type']==='smtp') ? '' : 'display:none'; ?>">
            <div class="form-group">
              <label>Host</label>
              <select name="host" id="pf_host" style="height:120px;">
                <?php
                  $predefinedHosts = [
                    'smtp.gmail.com',
                    'smtp.office365.com',
                    'smtp.mail.yahoo.com',
                    'smtp.zoho.com',
                    'smtp.sendgrid.net',
                    'smtp.mailgun.org',
                    'smtp.sparkpostmail.com',
                    'smtp.mailjet.com',
                    'email-smtp.us-east-1.amazonaws.com',
                  ];
                  $currentHost = $editProfile ? h($editProfile['host']) : '';
                  foreach ($predefinedHosts as $host):
                ?>
                  <option value="<?php echo h($host); ?>" <?php if ($currentHost === $host) echo 'selected'; ?>>
                    <?php echo h($host); ?>
                  </option>
                <?php endforeach; ?>
                <?php if ($currentHost !== '' && !in_array($currentHost, $predefinedHosts)): ?>
                  <option value="<?php echo h($currentHost); ?>" selected><?php echo h($currentHost); ?></option>
                <?php endif; ?>
              </select>
              <small class="hint">Select a predefined SMTP host or enter a custom host below.</small>
              <input type="text" name="custom_host" placeholder="Or enter custom host" style="margin-top:8px;" value="">
            </div>
            <div class="form-row">
              <div class="form-group">
                <label>Port</label>
                <input type="number" name="port" value="<?php echo $editProfile ? (int)$editProfile['port'] : 587; ?>">
              </div>
              <div class="form-group">
                <label>Username</label>
                <input type="text" name="username" value="<?php echo $editProfile ? h($editProfile['username']) : ''; ?>">
              </div>
            </div>
            <div class="form-group">
              <label>Password</label>
              <input type="text" name="password" value="<?php echo $editProfile ? h($editProfile['password']) : ''; ?>">
            </div>

            <div style="margin-top:8px; border-top:1px dashed var(--sg-border); padding-top:8px;">
              <div class="form-group">
                <label>Bounce IMAP Server (optional)</label>
                <input type="text" name="bounce_imap_server" placeholder="e.g. {imap.example.com:993/imap/ssl}INBOX" value="<?php echo $editProfile ? h($editProfile['bounce_imap_server'] ?? '') : ''; ?>">
                <small class="hint">Provide full IMAP mailbox string (or host) to scan bounces. Leave blank to skip.</small>
              </div>
              <div class="form-row">
                <div class="form-group">
                  <label>Bounce IMAP User</label>
                  <input type="text" name="bounce_imap_user" value="<?php echo $editProfile ? h($editProfile['bounce_imap_user'] ?? '') : ''; ?>">
                </div>
                <div class="form-group">
                  <label>Bounce IMAP Pass</label>
                  <input type="text" name="bounce_imap_pass" value="<?php echo $editProfile ? h($editProfile['bounce_imap_pass'] ?? '') : ''; ?>">
                </div>
              </div>
            </div>
          </div>

          <div id="pf_api_fields" style="<?php echo ($editProfile && $editProfile['type']==='api') ? '' : 'display:none'; ?>">
            <div class="form-group">
              <label>API Key</label>
              <input type="text" name="api_key" value="<?php echo $editProfile ? h($editProfile['api_key']) : ''; ?>">
            </div>
            <div class="form-group">
              <label>API URL</label>
              <input type="text" name="api_url" value="<?php echo $editProfile ? h($editProfile['api_url']) : ''; ?>">
            </div>
            <div class="form-group">
              <label>Headers JSON (optional)</label>
              <textarea name="headers_json" rows="3"><?php echo $editProfile ? h($editProfile['headers_json']) : ''; ?></textarea>
            </div>
          </div>

          <!-- Proxy Settings -->
          <div style="margin-top:12px; border-top:1px dashed var(--sg-border); padding-top:12px;">
            <div class="sidebar-section-title" style="margin-bottom:8px;">Proxy Settings</div>
            <div class="checkbox-row" style="margin-bottom:8px;">
              <input type="checkbox" name="proxy_enabled" id="pf_proxy_enabled" value="1"
                <?php if ($editProfile && !empty($editProfile['proxy_enabled'])) echo 'checked'; ?>>
              <label for="pf_proxy_enabled">Enable Proxy</label>
            </div>
            <div id="pf_proxy_fields" style="<?php echo ($editProfile && !empty($editProfile['proxy_enabled'])) ? '' : 'display:none'; ?>">
              <div class="form-row">
                <div class="form-group">
                  <label>Proxy Type</label>
                  <select name="proxy_type">
                    <?php
                      $ptype = $editProfile ? ($editProfile['proxy_type'] ?? 'http') : 'http';
                    ?>
                    <option value="http"   <?php if ($ptype === 'http')   echo 'selected'; ?>>HTTP</option>
                    <option value="socks4" <?php if ($ptype === 'socks4') echo 'selected'; ?>>SOCKS4</option>
                    <option value="socks5" <?php if ($ptype === 'socks5') echo 'selected'; ?>>SOCKS5</option>
                  </select>
                </div>
                <div class="form-group">
                  <label>Proxy Port</label>
                  <input type="number" name="proxy_port" min="1" max="65535" value="<?php echo $editProfile ? (int)($editProfile['proxy_port'] ?? 8080) : 8080; ?>">
                </div>
              </div>
              <div class="form-group">
                <label>Proxy Host / IP</label>
                <input type="text" name="proxy_host" placeholder="e.g. 123.45.67.89 or proxy.example.com" value="<?php echo $editProfile ? h($editProfile['proxy_host'] ?? '') : ''; ?>">
              </div>
              <div class="form-row">
                <div class="form-group">
                  <label>Proxy Username (optional)</label>
                  <input type="text" name="proxy_user" value="<?php echo $editProfile ? h($editProfile['proxy_user'] ?? '') : ''; ?>">
                </div>
                <div class="form-group">
                  <label>Proxy Password (optional)</label>
                  <input type="password" name="proxy_pass" value="<?php echo $editProfile ? h($editProfile['proxy_pass'] ?? '') : ''; ?>">
                </div>
              </div>
              <small class="hint">All API connections for this profile will be routed through the proxy.</small>
              <div style="margin-top:10px; display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
                <button type="button" id="pf_proxy_test_btn" class="btn btn-outline" style="font-size:12px; padding:4px 12px;">
                  🔍 Test Proxy &amp; Show IP
                </button>
                <span id="pf_proxy_test_result" style="font-size:12px;"></span>
              </div>
            </div>
          </div>

          <div class="form-row">
            <div class="form-group">
              <label>Workers (Minimum Override)</label>
              <input type="number" name="workers" min="<?php echo MIN_WORKERS; ?>" value="<?php echo $editProfile ? (int)($editProfile['workers'] ?? DEFAULT_WORKERS) : DEFAULT_WORKERS; ?>">
              <small class="hint">
                <strong>Auto-calculated:</strong> Workers are automatically calculated based on email count (16 workers per 1,000 emails for maximum speed). 
                This setting acts as a <strong>minimum override</strong> - the system will use whichever is higher: auto-calculated or this value.
                <br><br>
                <strong>Examples with optimized high-performance settings:</strong>
                • 1,000 emails = 16 workers automatically
                • 10,000 emails = 160 workers automatically  
                • 79,258 emails = 1,280 workers automatically (utilizing full 128GB RAM server capacity)
                <br><br>
                Values above 200 will trigger warnings in logs. Higher values increase speed significantly and utilize available system resources.
              </small>
            </div>
          </div>

          <div class="form-row">
            <div class="form-group">
              <label>Messages Per Worker</label>
              <input type="number" name="messages_per_worker" min="<?php echo MIN_MESSAGES_PER_WORKER; ?>" max="<?php echo MAX_MESSAGES_PER_WORKER; ?>" value="<?php echo $editProfile ? (int)($editProfile['messages_per_worker'] ?? DEFAULT_MESSAGES_PER_WORKER) : DEFAULT_MESSAGES_PER_WORKER; ?>">
              <small class="hint">Batch size for round-robin distribution (<?php echo MIN_MESSAGES_PER_WORKER; ?>-<?php echo MAX_MESSAGES_PER_WORKER; ?>, default: <?php echo DEFAULT_MESSAGES_PER_WORKER; ?>). Controls how emails are distributed across workers in batches. Example: 100 emails with Workers=2 and Messages=10 distributes in rounds, each worker getting 10 emails per round until all sent.</small>
            </div>
          </div>

          <div class="form-row">
            <div class="form-group">
              <label>Cycle Delay (milliseconds)</label>
              <input type="number" name="cycle_delay_ms" min="<?php echo MIN_CYCLE_DELAY_MS; ?>" max="<?php echo MAX_CYCLE_DELAY_MS; ?>" value="<?php echo $editProfile ? (int)($editProfile['cycle_delay_ms'] ?? DEFAULT_CYCLE_DELAY_MS) : DEFAULT_CYCLE_DELAY_MS; ?>">
              <small class="hint">Delay between worker processing cycles in milliseconds (<?php echo MIN_CYCLE_DELAY_MS; ?>-<?php echo MAX_CYCLE_DELAY_MS; ?>, default: <?php echo DEFAULT_CYCLE_DELAY_MS; ?>). This adds a pause between each round of email distribution across workers. Example: 100ms delay adds a brief pause between distribution cycles. Use 0 for no delay (maximum speed).</small>
            </div>
          </div>

          <div class="checkbox-row">
            <input type="checkbox" name="active" id="pf_active" <?php if(!$editProfile || $editProfile['active']) echo 'checked'; ?>>
            <label for="pf_active">Active</label>
          </div>

          <button class="btn btn-primary" type="submit"><?php echo $editProfile ? 'Save Changes' : 'Create Profile'; ?></button>
        </form>
      </div>
      <div class="sidebar-foot">
        <div class="hint">Rotation + profiles apply globally to all Single Sends. Configure bounce mailbox to auto-add bounces to unsubscribes.</div>
      </div>
    </div>
  </div>

  <!-- MAIN PAGE -->
  <div class="page-wrapper" id="pageWrapper">
    <div class="topbar">
      <div class="brand">
        <img src="mafia-single-sends-logo.png" alt="Logo" style="width:250px;height:100px;">
      </div>
      <div class="topbar-actions">
        <?php if ($page === 'list'): ?>
          <a href="?page=contacts" class="btn btn-outline">Contacts</a>
          <button class="btn btn-outline" id="spOpenBtn" type="button">Sending Profiles ⚙</button>
        <?php elseif ($page === 'contacts'): ?>
          <a href="?page=list" class="btn btn-outline">Single Sends</a>
        <?php else: ?>
          <a href="?page=list" class="btn btn-outline">Single Sends</a>
          <a href="?page=contacts" class="btn btn-outline">Contacts</a>
        <?php endif; ?>
      </div>
    </div>

    <div class="main-content">
      <?php if ($page === 'list'): ?>
        <?php
          // Check if PDO is available
          if ($pdo === null) {
              echo '<div class="page-title">Database Error</div>';
              echo '<div class="card"><div style="padding: 20px;">Database connection not available. Please check your configuration.</div></div>';
          } else {
              $stmt = $pdo->query("SELECT * FROM campaigns ORDER BY created_at DESC");
              $campaigns = $stmt->fetchAll(PDO::FETCH_ASSOC);
        ?>
        <div class="page-title">Single Sends</div>
        <div class="page-subtitle">Design, review, and send one-off campaigns, just like SendGrid.</div>

        <div class="card">
          <div class="card-header">
            <div>
              <div class="card-title">Campaigns</div>
              <div class="card-subtitle">
                Draft and sent Single Sends with live stats.
                <span id="activeCampaignsIndicator" style="display:none; margin-left:8px; color:#4CAF50; font-weight:600;">
                  <span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:#4CAF50; animation:pulse 2s infinite; margin-right:4px;"></span>
                  <span id="activeCampaignCount">0</span> campaign(s) sending
                </span>
              </div>
            </div>
            <form method="post" style="display:flex; gap:8px; align-items:center;">
              <input type="hidden" name="action" value="create_campaign">
              <input type="text" name="name" placeholder="Campaign name" style="font-size:13px; padding:6px 8px;">
              <button type="submit" class="btn btn-primary">+ Create Single Send</button>
            </form>
          </div>

          <!-- Bulk actions toolbar -->
          <div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;">
            <form method="post" id="bulkActionsForm" style="display:flex; gap:8px; align-items:center;">
              <input type="hidden" name="action" value="bulk_campaigns">
              <select name="bulk_action" id="bulkActionSelect" style="padding:6px;">
                <option value="">Bulk actions</option>
                <option value="delete_selected">Delete selected</option>
                <option value="duplicate_selected">Duplicate selected</option>
              </select>
              <button type="submit" class="btn btn-outline">Apply</button>
            </form>

            <div style="margin-left:auto; display:flex; gap:8px;">
              <form method="post" onsubmit="return confirm('Delete ALL unsubscribes? This will remove every address from unsubscribes table.');">
                <input type="hidden" name="action" value="delete_unsubscribes">
                <button class="btn btn-outline" type="submit">Clear Unsubscribes</button>
              </form>
              <form method="post" onsubmit="return confirm('Delete ALL bounce events? This will remove all bounce events from events table.');">
                <input type="hidden" name="action" value="delete_bounces">
                <button class="btn btn-outline" type="submit">Clear Bounces</button>
              </form>
            </div>
          </div>

          <form method="post" id="campaignsTableForm">
            <input type="hidden" name="action" value="bulk_campaigns">
            <table class="table">
              <thead>
                <tr>
                  <th style="width:36px;"><input type="checkbox" id="chkAll"></th>
                  <th>Campaign</th>
                  <th>Status</th>
                  <th>Subject</th>
                  <th>Sent</th>
                  <th>Target</th>
                  <th>Bounces</th>
                  <th>Delivered</th>
                  <th>Opens</th>
                  <th>Clicks</th>
                  <th>Updated</th>
                  <th>Actions</th>
                </tr>
              </thead>
              <tbody>
                <?php if (empty($campaigns)): ?>
                  <tr>
                    <td colspan="12" style="text-align:center; padding:20px; color:var(--sg-muted);">
                      No campaigns yet. Create your first Single Send above.
                    </td>
                  </tr>
                                 <?php else: ?>
                  <?php foreach ($campaigns as $c):
                    $stats = get_campaign_stats($pdo, (int)$c['id']);
                    $status = $c['status'];
                    // Support queued status: Grey (draft) → Orange (queued) → Yellow (sending) → Green (sent)
                    if ($status === 'sent') {
                        $dotClass = 'status-sent';
                    } elseif ($status === 'sending') {
                        $dotClass = 'status-sending';
                    } elseif ($status === 'queued') {
                        $dotClass = 'status-queued';
                    } else {
                        $dotClass = 'status-draft';
                    }
                    $link = ($status === 'sent' || $status === 'sending' || $status === 'queued') ? '?page=stats&id='.$c['id'] : '?page=editor&id='.$c['id'];
                  ?>
                    <tr data-cid="<?php echo (int)$c['id']; ?>">
                      <td><input type="checkbox" name="campaign_ids[]" value="<?php echo (int)$c['id']; ?>" class="campaign-checkbox"></td>
                      <td>
                        <a href="<?php echo $link; ?>">
                          <span id="status-dot-<?php echo (int)$c['id']; ?>" class="status-dot <?php echo $dotClass; ?>"></span>
                          <?php echo h($c['name']); ?>
                        </a>
                      </td>
                      <td><span id="status-label-<?php echo (int)$c['id']; ?>"><?php echo ucfirst($status); ?></span>
                        <?php if ($status === 'sending' || $status === 'queued'): ?>
                          <div id="progress-bar-<?php echo (int)$c['id']; ?>" style="margin-top:4px;">
                            <!-- ENHANCED: Improved progress bar with gradient and animation -->
                            <div style="background:linear-gradient(90deg, #E4E7EB 0%, #CBD2D9 100%); height:6px; border-radius:3px; overflow:hidden; box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);">
                              <div id="progress-fill-<?php echo (int)$c['id']; ?>" style="background:linear-gradient(90deg, #1A82E2 0%, #3B9EF3 100%); height:100%; width:0%; transition:width 0.5s ease-out, background 0.3s; box-shadow:0 0 4px rgba(26,130,226,0.3);"></div>
                            </div>
                            <div id="progress-text-<?php echo (int)$c['id']; ?>" style="font-size:11px; color:var(--sg-muted); margin-top:3px; font-weight:500;">
                              <span style="display:inline-flex; align-items:center; gap:4px;">
                                <span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:#4CAF50; animation:pulse 2s infinite;" title="Active"></span>
                                0% • 0/0
                              </span>
                            </div>
                            <div style="margin-top:4px;">
                              <span id="workers-badge-<?php echo (int)$c['id']; ?>" style="display:inline-flex; align-items:center; gap:4px; background:linear-gradient(135deg,#1A82E2,#0e5fad); color:#fff; border-radius:12px; padding:2px 8px; font-size:11px; font-weight:700; box-shadow:0 1px 4px rgba(26,130,226,0.4);">
                                <span style="display:inline-block; width:6px; height:6px; border-radius:50%; background:#7fffb0; animation:pulse 1s infinite;"></span>
                                <span id="workers-count-<?php echo (int)$c['id']; ?>">0</span> workers active
                              </span>
                            </div>
                          </div>
                        <?php endif; ?>
                      </td>
                      <td><?php echo h($c['subject']); ?></td>
                      <td><?php echo $c['sent_at'] ? h($c['sent_at']) : '—'; ?></td>
                      <td><?php echo (int)$stats['target']; ?></td>
                      <td><span id="bounce-<?php echo (int)$c['id']; ?>"><?php echo (int)$stats['bounce']; ?></span></td>
                      <td><span id="delivered-<?php echo (int)$c['id']; ?>"><?php echo (int)$stats['delivered']; ?></span></td>
                      <td><span id="open-<?php echo (int)$c['id']; ?>"><?php echo (int)$stats['open']; ?></span></td>
                      <td><span id="click-<?php echo (int)$c['id']; ?>"><?php echo (int)$stats['click']; ?></span></td>
                      <td><?php echo h($c['created_at'] ?? ''); ?></td>
                      <td>
                        <a class="btn-mini" href="<?php echo $link; ?>">Open</a>
                        <form method="post" style="display:inline;">
                          <input type="hidden" name="action" value="duplicate_campaign">
                          <input type="hidden" name="campaign_id" value="<?php echo (int)$c['id']; ?>">
                          <button type="submit" class="btn-mini">Duplicate</button>
                        </form>
                        <form method="post" style="display:inline;">
                          <input type="hidden" name="action" value="delete_campaign">
                          <input type="hidden" name="campaign_id" value="<?php echo (int)$c['id']; ?>">
                          <button type="submit" class="btn-mini" onclick="return confirm('Delete this campaign and its events?');">Delete</button>
                        </form>
                      </td>
                    </tr>
                  <?php endforeach; ?>
                <?php endif; ?>
              </tbody>
            </table>
          </form>
        </div>

        <script>
          (function(){
            // If URL contains sent_campaign=<id>, start polling its stats to show incremental delivered updates
            function getQueryParam(name) {
              var params = new URLSearchParams(window.location.search);
              return params.get(name);
            }
            var sentCampaign = getQueryParam('sent_campaign');
            if (sentCampaign) {
              var cid = sentCampaign;
              var interval = 2000;
              var timer = setInterval(function(){
                fetch(window.location.pathname + '?ajax=campaign_stats&id=' + encodeURIComponent(cid))
                  .then(function(r){ return r.json(); })
                  .then(function(data){
                    if (!data) return;
                    var d = document.getElementById('delivered-' + cid);
                    var o = document.getElementById('open-' + cid);
                    var c = document.getElementById('click-' + cid);
                    if (d) d.innerText = data.delivered || 0;
                    if (o) o.innerText = data.open || 0;
                    if (c) c.innerText = data.click || 0;
                  }).catch(function(){});
              }, interval);
              // stop polling after 10 minutes as safety
              setTimeout(function(){ clearInterval(timer); }, 10 * 60 * 1000);
            }

            // OPTIMIZED: Real-time progress polling with adaptive intervals
            var sendingCampaigns = document.querySelectorAll('[data-cid]');
            var progressPollers = {};
            
            sendingCampaigns.forEach(function(row){
              var cid = row.getAttribute('data-cid');
              var statusDot = row.querySelector('.status-dot');
              
              if (statusDot && (statusDot.classList.contains('status-sending') || statusDot.classList.contains('status-queued'))) {
                // PERFORMANCE: Use adaptive polling - fast initially, slower as campaign progresses
                var pollCount = 0;
                var consecutiveUnchanged = 0;
                var lastPercentage = 0;
                var currentPollInterval = 2000;
                
                var pollProgress = function(){
                  var fd = new FormData();
                  fd.append('action', 'get_campaign_progress');
                  fd.append('campaign_id', cid);
                  
                  fetch(window.location.pathname, {
                    method: 'POST',
                    body: fd,
                    credentials: 'same-origin'
                  })
                  .then(function(r){ return r.json(); })
                  .then(function(data){
                    if (!data || !data.ok) return;
                    
                    pollCount++;
                    
                    // Track if percentage changed to adapt polling rate
                    if (data.percentage === lastPercentage) {
                      consecutiveUnchanged++;
                    } else {
                      consecutiveUnchanged = 0;
                      lastPercentage = data.percentage;
                      // Speed back up to fast polling when progress resumes
                      if (currentPollInterval !== 2000 && progressPollers[cid]) {
                        currentPollInterval = 2000;
                        clearInterval(progressPollers[cid]);
                        progressPollers[cid] = setInterval(pollProgress, 2000);
                      }
                    }
                    
                    // PERFORMANCE: Update DOM elements efficiently (batch DOM updates)
                    requestAnimationFrame(function(){
                      var fillEl = document.getElementById('progress-fill-' + cid);
                      var textEl = document.getElementById('progress-text-' + cid);
                      var deliveredEl = document.getElementById('delivered-' + cid);
                      var bounceEl = document.getElementById('bounce-' + cid);
                      var workersCountEl = document.getElementById('workers-count-' + cid);
                      var workersBadgeEl = document.getElementById('workers-badge-' + cid);
                      var dotEl = document.getElementById('status-dot-' + cid);
                      var labelEl = document.getElementById('status-label-' + cid);
                      
                      if (fillEl) {
                        fillEl.style.width = data.percentage + '%';
                      }
                      if (textEl) {
                        textEl.innerHTML = '<span style="display:inline-flex; align-items:center; gap:4px;"><span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:#4CAF50; animation:pulse 2s infinite;" title="Active"></span>' + data.percentage + '% \u2022 ' + data.total_processed + '/' + data.total_recipients + '</span>';
                      }
                      if (workersCountEl) {
                        workersCountEl.innerText = data.active_workers;
                      }
                      if (workersBadgeEl) {
                        workersBadgeEl.style.opacity = data.active_workers === 0 ? '0.4' : '1';
                      }
                      if (deliveredEl) {
                        deliveredEl.innerText = data.delivered || 0;
                      }
                      if (bounceEl) {
                        bounceEl.innerText = data.bounced || 0;
                      }
                      // Update status dot and label without page reload when queued→sending
                      if (data.status === 'sending') {
                        if (dotEl && !dotEl.classList.contains('status-sending')) {
                          dotEl.className = 'status-dot status-sending';
                        }
                        if (labelEl && labelEl.textContent !== 'Sending') {
                          labelEl.textContent = 'Sending';
                        }
                      }
                    });
                    
                    // If campaign finished, stop polling and reload page
                    var isProcessingComplete = data.progress_status === 'completed' ||
                                             (data.total_recipients > 0 && data.total_processed >= data.total_recipients);
                    
                    if (data.status === 'sent') {
                      // Status is already 'sent', safe to reload immediately
                      clearInterval(progressPollers[cid]);
                      delete progressPollers[cid];
                      
                      setTimeout(function(){
                        window.location.reload();
                      }, 1000);
                    } else if (isProcessingComplete) {
                      // Processing is complete but status not yet updated
                      if (!progressPollers[cid + '_waiting']) {
                        progressPollers[cid + '_waiting'] = Date.now();
                      }
                      
                      // If we've been waiting more than 10 seconds, reload anyway
                      var waitTime = Date.now() - progressPollers[cid + '_waiting'];
                      if (waitTime > 10000) {
                        clearInterval(progressPollers[cid]);
                        delete progressPollers[cid];
                        delete progressPollers[cid + '_waiting'];
                        window.location.reload();
                      }
                    }
                    
                    // PERFORMANCE: Adaptive polling interval
                    // If no changes for 3+ consecutive polls, slow down polling
                    if (consecutiveUnchanged >= 3 && currentPollInterval !== 5000 && progressPollers[cid]) {
                      currentPollInterval = 5000;
                      clearInterval(progressPollers[cid]);
                      // Slower polling (every 5 seconds) when no changes detected
                      progressPollers[cid] = setInterval(pollProgress, 5000);
                    }
                  })
                  .catch(function(err){
                    console.error('Progress poll error for campaign ' + cid + ':', err);
                    // On error (e.g. proxy disconnect), reset to fast polling so recovery is quick
                    if (currentPollInterval !== 2000 && progressPollers[cid]) {
                      currentPollInterval = 2000;
                      consecutiveUnchanged = 0;
                      clearInterval(progressPollers[cid]);
                      progressPollers[cid] = setInterval(pollProgress, 2000);
                    }
                  });
                };
                
                // Start with fast polling (every 2 seconds)
                progressPollers[cid] = setInterval(pollProgress, 2000);
                // Initial poll immediately
                pollProgress();
                
                // PERFORMANCE: Stop polling after 20 minutes as safety
                setTimeout(function(){
                  if (progressPollers[cid]) {
                    clearInterval(progressPollers[cid]);
                    delete progressPollers[cid];
                  }
                }, 20 * 60 * 1000);
              }
            });

            // ENHANCEMENT: Update active campaigns indicator
            var updateActiveCampaignsIndicator = function(){
              var activeCount = Object.keys(progressPollers).filter(function(k){ return !k.includes('_waiting'); }).length;
              var indicator = document.getElementById('activeCampaignsIndicator');
              var countSpan = document.getElementById('activeCampaignCount');
              
              if (indicator && countSpan) {
                if (activeCount > 0) {
                  indicator.style.display = '';
                  countSpan.textContent = activeCount;
                } else {
                  indicator.style.display = 'none';
                }
              }
            };
            
            // Update indicator immediately and periodically
            updateActiveCampaignsIndicator();
            setInterval(updateActiveCampaignsIndicator, 5000);

            // select all checkbox
            var chkAll = document.getElementById('chkAll');
            if (chkAll) {
              chkAll.addEventListener('change', function(){
                var boxes = document.querySelectorAll('.campaign-checkbox');
                boxes.forEach(function(b){ b.checked = chkAll.checked; });
              });
            }

            // bulk actions form should copy selected ids into its own form when submitted
            var bulkForm = document.getElementById('bulkActionsForm');
            bulkForm.addEventListener('submit', function(e){
              var sel = document.querySelectorAll('.campaign-checkbox:checked');
              if (sel.length === 0) {
                alert('No campaigns selected.');
                e.preventDefault();
                return false;
              }
              // create hidden inputs for each selected id
              // remove previous if any
              Array.from(bulkForm.querySelectorAll('input[name="campaign_ids[]"]')).forEach(function(n){ n.remove(); });
              sel.forEach(function(cb){
                var inp = document.createElement('input');
                inp.type = 'hidden';
                inp.name = 'campaign_ids[]';
                inp.value = cb.value;
                bulkForm.appendChild(inp);
              });
            });
          })();
        </script>
        <?php } // End PDO check ?>

      <?php elseif ($page === 'editor'): ?>
        <?php
          if ($pdo === null) {
              echo '<div class="page-title">Database Error</div>';
              echo '<div class="card"><div style="padding: 20px;">Database connection not available. Please check your configuration.</div></div>';
          } else {
              $id = (int)($_GET['id'] ?? 0);
              $campaign = get_campaign($pdo, $id);
              if (!$campaign) {
                echo "<p>Campaign not found.</p>";
              } else {
                $rotOnEditor = (int)$rotationSettings['rotation_enabled'] === 1;
        ?>
          <div class="page-title">Editor — <?php echo h($campaign['name']); ?></div>
          <div class="page-subtitle">Define subject, from, preheader and design your email content.</div>

          <!-- Editor shell: left modules + envelope, right canvas -->
          <form method="post" id="editorForm">
            <input type="hidden" name="action" value="save_campaign">
            <input type="hidden" name="id" value="<?php echo (int)$campaign['id']; ?>">

            <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
              <div style="display:flex; gap:12px; align-items:center;">
                <button class="btn btn-outline" type="button" id="saveBtnTop">Save</button>
                <button class="btn btn-outline" type="button" id="undoBtn" disabled>Undo</button>
                <button class="btn btn-outline" type="button" id="redoBtn" disabled>Redo</button>
              </div>
              <div>
                <!-- single Review button -->
                <button class="btn btn-primary" type="button" id="reviewTopBtn">Review Details and Send →</button>
              </div>
            </div>

            <div class="editor-shell">
              <!-- LEFT: Modules + Envelope -->
              <div class="editor-left" role="region" aria-label="Design left sidebar">
                <div class="editor-tabs">
                  <div class="tab active" data-tab="build">Build</div>
                  <div class="tab" data-tab="settings">Settings</div>
                  <div class="tab" data-tab="tags">Tags</div>
                  <div class="tab" data-tab="ab">A/B Testing</div>
                </div>

                <div class="modules-pane" id="modulesPane">
                  <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
                    <div style="font-weight:600; color:var(--sg-muted);">Add Modules</div>
                    <div style="color:var(--sg-muted); font-size:13px;">Drag or click → drop into the canvas</div>
                  </div>
                  <div class="modules-grid" id="modulesGrid">
                    <div class="module-tile" data-module="image"><div class="icon">🖼️</div>Image</div>
                    <div class="module-tile" data-module="text"><div class="icon">✏️</div>Text</div>
                    <div class="module-tile" data-module="columns"><div class="icon">▦</div>Columns</div>
                    <div class="module-tile" data-module="imgtext"><div class="icon">🧩</div>Image &amp; Text</div>
                    <div class="module-tile" data-module="button"><div class="icon">🔘</div>Button</div>
                    <div class="module-tile" data-module="code"><div class="icon">&lt;&gt;</div>Code</div>
                    <div class="module-tile" data-module="divider"><div class="icon">─</div>Divider</div>
                    <div class="module-tile" data-module="spacer"><div class="icon">↕️</div>Spacer</div>
                    <div class="module-tile" data-module="social"><div class="icon">⚑</div>Social</div>
                    <div class="module-tile" data-module="unsubscribe"><div class="icon">✖</div>Unsubscribe</div>
                  </div>

                </div>

                <div class="envelope">
                  <div style="font-weight:600; margin-bottom:8px;">Envelope</div>
                  <div class="form-group">
                    <label>Subject</label>
                    <input type="text" name="subject" value="<?php echo h($campaign['subject']); ?>" required>
                  </div>
                  <div class="form-group">
                    <label>Preheader</label>
                    <input type="text" name="preheader" value="<?php echo h($campaign['preheader']); ?>">
                  </div>

                  <div class="form-group">
                    <label>
                      From
                      <?php if ($rotOnEditor): ?>
                        <span style="font-weight:400; color:var(--sg-muted);">(taken from each active profile while rotation is ON)</span>
                      <?php endif; ?>
                    </label>
                    <?php if ($rotOnEditor): ?>
                      <select disabled>
                        <option>Rotation enabled — using each profile's From</option>
                      </select>
                    <?php else: ?>
                      <select name="from_email" required>
                        <option value="">Select from Sending Profiles</option>
                        <?php foreach ($profiles as $p): ?>
                          <option value="<?php echo h($p['from_email']); ?>"
                            <?php if ($campaign['from_email'] === $p['from_email']) echo 'selected'; ?>>
                            <?php echo h($p['from_email'].' — '.$p['profile_name']); ?>
                          </option>
                        <?php endforeach; ?>
                      </select>
                    <?php endif; ?>
                  </div>

                  <div class="form-group">
                    <label>Audience (List / segment)</label>
                    <!-- audience now shows contact lists for selection -->
                    <select name="audience">
                      <option value="">Select a contact list or keep free text</option>
                      <?php foreach ($contactLists as $cl):
                        $val = 'list:'.$cl['id'];
                        $selected = ($campaign['audience'] === $val) ? 'selected' : '';
                      ?>
                        <option value="<?php echo h($val); ?>" <?php echo $selected; ?>>
                          <?php echo h($cl['name']).' — '.(int)$cl['contact_count'].' contacts'; ?>
                        </option>
                      <?php endforeach; ?>
                      <option value="custom" <?php if ($campaign['audience'] === 'custom') echo 'selected'; ?>>Custom segment / manual</option>
                    </select>
                    <small class="hint">Choose one of your contact lists. If you need a text/segment, select "Custom" then edit audience in the campaign later.</small>
                  </div>

                  <div class="form-row">
                    <div class="form-group">
                      <label>Sender Name</label>
                      <input type="text" name="sender_name" value="<?php echo h($campaign['sender_name'] ?? ''); ?>">
                    </div>
                    <div class="form-group">
                      <label>Reply-To Email</label>
                      <input type="email" name="reply_to" placeholder="Leave empty to use From Email" value="<?php echo h($campaign['reply_to'] ?? ''); ?>">
                    </div>
                  </div>

                  <div class="form-row">
                    <div class="form-group">
                      <label>&nbsp;</label>
                      <div class="checkbox-row">
                        <input type="checkbox" name="unsubscribe_enabled" id="unsubscribe_enabled" <?php if (!empty($campaign['unsubscribe_enabled'])) echo 'checked'; ?>>
                        <label for="unsubscribe_enabled">Enable Unsubscribe Link</label>
                      </div>
                      <small class="hint">If enabled, an unsubscribe link is injected automatically.</small>
                    </div>
                    <div class="form-group">
                      <label>&nbsp;</label>
                      <div class="checkbox-row">
                        <input type="checkbox" name="preheader_injection_enabled" id="preheader_injection_enabled" <?php if ($campaign['preheader_injection_enabled'] ?? 1) echo 'checked'; ?>>
                        <label for="preheader_injection_enabled">Enable Preheader Ref</label>
                      </div>
                      <small class="hint">If enabled, a unique Ref token is injected as invisible inbox preview text (preheader).</small>
                    </div>
                    <div class="form-group">
                      <label>&nbsp;</label>
                      <div class="checkbox-row">
                        <input type="checkbox" name="footer_injection_enabled" id="footer_injection_enabled" <?php if ($campaign['footer_injection_enabled'] ?? 1) echo 'checked'; ?>>
                        <label for="footer_injection_enabled">Enable Footer Ref</label>
                      </div>
                      <small class="hint">If enabled, a unique Ref token is injected as a small grey line at the bottom of the email.</small>
                    </div>
                  </div>

                </div>
              </div>

              <!-- RIGHT: Canvas -->
              <div class="editor-right">
                <div class="editor-canvas-top">
                  <div class="canvas-header-left">
                    <div class="subj">Subject: <?php echo $campaign['subject'] ? h($campaign['subject']) : 'Subject'; ?></div>
                    <div class="pre">Preheader: <?php echo $campaign['preheader'] ? h($campaign['preheader']) : ''; ?></div>
                  </div>
                  <div class="canvas-header-right">
                    <button class="btn btn-outline" type="button" id="previewBtn">Preview</button>
                    <button class="btn btn-outline" type="button" id="saveAsTplBtn">Save</button>
                    <!-- single Review button at top -->
                  </div>
                </div>

                <!-- Content quality indicator (updated by JS whenever canvas changes) -->
                <div id="contentQualityBar" style="padding:6px 12px;font-size:12px;display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--sg-border);background:#fff;">
                  <span style="color:var(--sg-muted);">Content quality:</span>
                  <span id="cqBadge" style="font-weight:600;padding:2px 8px;border-radius:10px;font-size:11px;" role="status" aria-live="polite">Checking…</span>
                  <span id="cqTip" style="color:var(--sg-muted);font-size:11px;"></span>
                </div>

                <div class="canvas-area" id="canvasArea" data-placeholder="Drag Module Here">
                  <?php
                    // initial canvas content will be hydrated by JS from campaign.html
                    if ($campaign['html']) {
                        echo '<!-- stored html will be loaded into blocks by JS -->';
                    } else {
                        echo '<div class="drag-placeholder">Drag Module Here</div>';
                    }
                  ?>
                </div>

                <div class="canvas-footer">
                  <div style="max-width:780px; margin:0 auto;">
                    <?php if (!empty($campaign['sender_name'])): ?>
                      <?php echo h($campaign['sender_name']); ?>
                    <?php else: ?>
                      <!-- no address lines as requested; show nothing if no sender_name -->
                    <?php endif; ?>
                    <?php if (!empty($campaign['unsubscribe_enabled'])): ?>
                      &nbsp;·&nbsp;<a href="#">Unsubscribe</a>
                    <?php endif; ?>
                  </div>
                </div>
              </div>
            </div>

            <!-- Hidden html textarea (holds campaign html) -->
            <textarea name="html" id="htmlField" style="display:none;"><?php echo h($campaign['html']); ?></textarea>

            <!-- hidden bottom buttons kept for compatibility -->
            <div style="display:none; margin-top:12px; text-align:right;">
              <button type="submit" class="btn btn-outline">Save</button>
<!-- continuing from the snippet above -->
              <button type="submit" name="go_to_review" value="1" class="btn btn-primary">Review Details &amp; Send</button>
            </div>
          </form>

          <!-- HTML Editor Modal (for Code module) -->
          <div class="html-editor-backdrop" id="htmlEditorBackdrop" role="dialog" aria-modal="true">
            <div class="html-editor-panel" role="document">
              <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
                <div style="font-weight:600;">HTML Editor</div>
                <button class="btn btn-outline" id="htmlEditorClose">✕</button>
              </div>
              <textarea id="htmlEditorTextarea"><?php echo h($campaign['html']); ?></textarea>
              <div class="html-editor-actions">
                <button class="btn btn-outline" id="htmlEditorCancel">Cancel</button>
                <button class="btn btn-primary" id="htmlEditorSave">Save HTML</button>
              </div>
            </div>
          </div>

          <!-- Include SortableJS for drag & drop -->
          <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>

          <script>
            (function(){
              // Helpers
              function $(sel, root){ return (root || document).querySelector(sel); }
              function $all(sel, root){ return Array.from((root || document).querySelectorAll(sel)); }

              var reviewTop = document.getElementById('reviewTopBtn');
              var form = document.getElementById('editorForm');
              var saveBtnTop = document.getElementById('saveBtnTop');

              // HTML Editor modal elems (declare early so handlers can reference them)
              var htmlBackdrop = document.getElementById('htmlEditorBackdrop');
              var htmlTextarea = document.getElementById('htmlEditorTextarea');
              var htmlClose = document.getElementById('htmlEditorClose');
              var htmlCancel = document.getElementById('htmlEditorCancel');
              var htmlSave = document.getElementById('htmlEditorSave');

              // Toast utility
              function showToast(msg, type){
                var t = document.getElementById('globalToast');
                if(!t) return;
                t.innerText = msg;
                t.className = 'toast show ' + (type === 'error' ? 'error' : 'success');
                setTimeout(function(){
                  t.className = 'toast';
                }, 2000);
              }

              // Save behavior (top save) - use AJAX to enable autosave UX
              function doSaveAjax(callback){
                syncCanvasToHtmlField();
                var fd = new FormData(form);
                // Ensure action=save_campaign present
                fd.set('action', 'save_campaign');

                fetch(window.location.pathname + window.location.search, {
                  method: 'POST',
                  body: fd,
                  credentials: 'same-origin',
                  headers: {
                    'X-Requested-With': 'XMLHttpRequest'
                  }
                }).then(function(resp){
                  if (!resp.ok) throw new Error('Save failed');
                  return resp.json().catch(function(){ return {ok:1}; });
                }).then(function(json){
                  showToast('Saved', 'success');
                  if (typeof callback === 'function') callback(null, json);
                }).catch(function(err){
                  showToast('Save error', 'error');
                  // fallback to normal submit if desired
                  if (confirm('Autosave failed, submit full form instead?')) {
                    form.submit();
                  }
                  if (typeof callback === 'function') callback(err);
                });
              }

              function doSave() {
                doSaveAjax();
              }
              if (saveBtnTop) saveBtnTop.addEventListener('click', doSave);

              // Autosave every 7 seconds (only on editor page)
              var autosaveTimer = setInterval(function(){
                // only run autosave if form exists and page is visible
                if (document.visibilityState === 'visible') {
                  doSaveAjax();
                }
              }, 7000);

              // When user clicks Review: if HTML modal is open, automatically save modal contents first
              function doReviewSubmit(){
                // if modal open, persist modal content programmatically (avoid invoking handlers via click())
                if (htmlBackdrop && htmlBackdrop.classList.contains('show')) {
                  try {
                    // Persist raw HTML into hidden field
                    if (typeof htmlField !== 'undefined' && htmlField !== null) {
                      htmlField.value = htmlTextarea.value;
                    } else {
                      // htmlField may be defined later; fallback to finding it now
                      var hf = document.getElementById('htmlField');
                      if (hf) hf.value = htmlTextarea.value;
                    }
                    // Rehydrate canvas from new HTML so syncCanvasToHtmlField picks latest content
                    if (typeof loadHtmlIntoCanvas === 'function') {
                      loadHtmlIntoCanvas(htmlTextarea.value);
                    }
                    // Close modal visually
                    if (typeof closeHtmlEditor === 'function') {
                      closeHtmlEditor();
                    } else {
                      htmlBackdrop.classList.remove('show');
                      htmlBackdrop.style.display = 'none';
                    }
                  } catch (e) {
                    // if anything goes wrong, gracefully continue to submit whatever we have
                  }

                  var hidden = document.createElement('input');
                  hidden.type = 'hidden';
                  hidden.name = 'go_to_review';
                  hidden.value = '1';
                  form.appendChild(hidden);
                  // ensure latest HTML is synced
                  syncCanvasToHtmlField();
                  form.submit();
                  return;
                }
                // otherwise normal submit
                var hidden = document.createElement('input');
                hidden.type = 'hidden';
                hidden.name = 'go_to_review';
                hidden.value = '1';
                form.appendChild(hidden);
                // ensure html saved from canvas
                syncCanvasToHtmlField();
                form.submit();
              }
              if (reviewTop) reviewTop.addEventListener('click', doReviewSubmit);

              // small tab UI (non-functional placeholders)
              var tabs = document.querySelectorAll('.editor-left .tab');
              tabs.forEach(function(t){
                t.addEventListener('click', function(){
                  tabs.forEach(function(x){ x.classList.remove('active'); });
                  t.classList.add('active');
                });
              });

              var modulesGrid = document.getElementById('modulesGrid');
              var modulesPane = document.getElementById('modulesPane');
              var canvas = document.getElementById('canvasArea');
              var htmlField = document.getElementById('htmlField');

              function openHtmlEditor() {
                htmlTextarea.value = htmlField.value || '';
                htmlBackdrop.classList.add('show');
                htmlBackdrop.style.display = 'flex';
                htmlTextarea.focus();
              }
              function closeHtmlEditor() {
                htmlBackdrop.classList.remove('show');
                htmlBackdrop.style.display = 'none';
              }

              // Build block markup for modules
              function createBlockHtmlByModule(module) {
                    switch(module) {
                      case 'image':
                        return '<div style="max-width:100%;text-align:center;">'
                             + '<img src="https://via.placeholder.com/600x200" alt="Image description" style="max-width:100%;">'
                             + '<p style="font-size:13px;color:#555555;margin:6px 0 0;">Add a caption or description for this image here.</p>'
                             + '</div>';
                      case 'text':
                        return '<div style="max-width:100%;font-size:15px;line-height:1.8;color:#333333;padding:8px 0;"><p>Write your message here. A well-written email with plenty of informative text will reach the inbox more reliably and engage your readers better.</p></div>';
                      case 'button':
                        return '<div style="text-align:center;margin:16px;"><a href="#" style="background:var(--sg-blue);color:#fff;padding:10px 24px;border-radius:4px;text-decoration:none;font-size:15px;display:inline-block;">Click Here</a></div>';
                      case 'columns':
                        return '<div style="display:flex;gap:10px;"><div style="flex:1;background:#fafafa;padding:10px;">Column 1 — add your content here.</div><div style="flex:1;background:#fafafa;padding:10px;">Column 2 — add your content here.</div></div>';
                      case 'imgtext':
                        return '<div style="display:flex;gap:12px;align-items:flex-start;">'
                             + '<img src="https://via.placeholder.com/160x100" alt="Feature image" style="width:160px;height:auto;border-radius:4px;">'
                             + '<div style="flex:1;font-size:14px;line-height:1.7;color:#333333;"><strong>Heading</strong><p>Describe this feature or news item in a sentence or two. Good text-to-image balance helps deliverability.</p></div>'
                             + '</div>';
                      case 'code':
                        return '<div class="code-module"><div class="code-module-header"><span></span><span class="tools"><button type="button" data-tool="edit" title="Edit code" style="background:transparent;border:none;color:#fff;cursor:pointer;">&lt;&gt;</button><button type="button" data-tool="delete" title="Delete" style="background:transparent;border:none;color:#fff;cursor:pointer;">🗑</button></span></div><div class="code-module-body"></div></div>';
                      default:
                        return '<div>Module: '+module+'</div>';
                    }
                  }

                  // Create canvas block element (wrapper with remove button)
                  // Accepts contentHtml or a module wrapper (like code-module)
                  function makeCanvasBlock(contentHtml) {
                    var wrapper = document.createElement('div');
                    wrapper.className = 'canvas-block';
                    var removeBtn = document.createElement('div');
                    removeBtn.className = 'block-remove';
                    removeBtn.title = 'Remove block';
                    removeBtn.innerHTML = '✕';
                    removeBtn.addEventListener('click', function(){
                      // confirm removal
                      if (confirm('Remove this block?')) {
                        wrapper.remove();
                        updatePlaceholderVisibility();
                      }
                    });

                    var content = document.createElement('div');
                    content.className = 'block-content';
                    content.innerHTML = contentHtml || '';

                    // If the block contains a code-module, wire up header tools and editing behavior
                    var codeModuleEl = content.querySelector('.code-module');
                    if (codeModuleEl) {
                      // mark dataset for later use
                      wrapper.dataset.module = 'code';

                      var header = codeModuleEl.querySelector('.code-module-header');
                      var body = codeModuleEl.querySelector('.code-module-body');

                      // tool handlers
                      header.addEventListener('click', function(e){
                        // if clicked on tool buttons handle separately
                        var t = e.target;
                        if (t && t.getAttribute && t.getAttribute('data-tool') === 'delete') {
                          if (confirm('Delete this code block?')) {
                            wrapper.remove();
                            updatePlaceholderVisibility();
                          }
                          return;
                        }
                        // open the block-level HTML editor for this code body
                        openBlockEditor(body);
                      });

                      // support double-click edit for code body as well
                      body.addEventListener('dblclick', function(){
                        openBlockEditor(body);
                      });
                    } else {
                      // double-click to edit via HTML editor (for general blocks)
                      content.addEventListener('dblclick', function(){
                        // open code editor with this block's HTML only
                        htmlTextarea.value = content.innerHTML;
                        htmlBackdrop.classList.add('show');
                        htmlBackdrop.style.display = 'flex';
                        var oneOff = function(e){
                          e.preventDefault();
                          content.innerHTML = htmlTextarea.value;
                          htmlBackdrop.classList.remove('show');
                          htmlBackdrop.style.display = 'none';
                          htmlSave.removeEventListener('click', oneOff);
                          if (defaultSaveHandler) {
                            htmlSave.addEventListener('click', defaultSaveHandler);
                          }
                          syncCanvasToHtmlField();
                        };
                        // replace
                        // replace handlers for this one-off edit
                        if (defaultSaveHandler) htmlSave.removeEventListener('click', defaultSaveHandler);
                        htmlSave.addEventListener('click', oneOff);
                      });
                    }

                    wrapper.appendChild(removeBtn);
                    wrapper.appendChild(content);
                    return wrapper;
                  }

                  // Open block-level HTML editor (edits only the body of a code module or a block container)
                  function openBlockEditor(bodyElement) {
                    // set the textarea to the current innerHTML of the block body
                    htmlTextarea.value = bodyElement.innerHTML;
                    htmlBackdrop.classList.add('show');
                    htmlBackdrop.style.display = 'flex';
                    htmlTextarea.focus();

                    // assign a one-off save handler that updates this bodyElement
                    var oneOff = function(e){
                      e.preventDefault();
                      bodyElement.innerHTML = htmlTextarea.value;
                      htmlBackdrop.classList.remove('show');
                      htmlBackdrop.style.display = 'none';
                      // restore global handler
                      htmlSave.removeEventListener('click', oneOff);
                      if (defaultSaveHandler) {
                        htmlSave.addEventListener('click', defaultSaveHandler);
                      }
                      syncCanvasToHtmlField();
                    };

                    // ensure previous onclick is cleared and attach oneOff
                    if (defaultSaveHandler) htmlSave.removeEventListener('click', defaultSaveHandler);
                    htmlSave.addEventListener('click', oneOff);
                  }

                  // Sync: read all canvas blocks and set htmlField
                  function syncCanvasToHtmlField() {
                    // take innerHTML of all .block-content inside canvas, concatenate
                    var blocks = canvas.querySelectorAll('.canvas-block .block-content');
                    if (blocks.length === 0) {
                      htmlField.value = '';
                      return;
                    }
                    var html = '';
                    blocks.forEach(function(b){
                      // Check if this block contains a code-module
                      var codeModule = b.querySelector('.code-module');
                      if (codeModule) {
                        // For code modules, extract only the body content (skip the header with emoji/buttons)
                        var codeBody = codeModule.querySelector('.code-module-body');
                        if (codeBody) {
                          html += codeBody.innerHTML;
                        } else {
                          // Fallback: if body not found, use full block content to avoid data loss
                          html += b.innerHTML;
                        }
                      } else {
                        // For non-code modules, include the entire block content
                        html += b.innerHTML;
                      }
                    });
                    htmlField.value = html;
                  }

                  // Update placeholder if empty
                  function updatePlaceholderVisibility() {
                    var hasBlocks = canvas.querySelectorAll('.canvas-block').length > 0;
                    var placeholder = canvas.querySelector('.drag-placeholder');
                    if (!hasBlocks) {
                      if (!placeholder) {
                        var ph = document.createElement('div');
                        ph.className = 'drag-placeholder';
                        ph.innerText = 'Drag Module Here';
                        canvas.appendChild(ph);
                      }
                    } else {
                      if (placeholder) placeholder.remove();
                    }
                  }

                  // Default html save handler for modal (saves entire canvas)
                  function defaultHtmlSaveHandler(e){
                    e.preventDefault();
                    // set hidden field and update canvas preview
                    htmlField.value = htmlTextarea.value;
                    // rehydrate canvas blocks from new html (single big block)
                    loadHtmlIntoCanvas(htmlTextarea.value);
                    closeHtmlEditor();
                  }
                  var defaultSaveHandler = defaultHtmlSaveHandler;
                  if (htmlSave) htmlSave.addEventListener('click', defaultSaveHandler);

                  if (htmlClose) htmlClose.addEventListener('click', function(){ closeHtmlEditor(); });
                  if (htmlCancel) htmlCancel.addEventListener('click', function(e){ e.preventDefault(); closeHtmlEditor(); });

                  // Parse existing htmlField value into canvas blocks (split top-level nodes)
                  function loadHtmlIntoCanvas(rawHtml) {
                    canvas.innerHTML = ''; // clear
                    if (!rawHtml || rawHtml.trim() === '') {
                      updatePlaceholderVisibility();
                      return;
                    }
                    var tmp = document.createElement('div');
                    tmp.innerHTML = rawHtml;
                    // if there are multiple top-level nodes, create a block for each
                    var children = Array.from(tmp.childNodes).filter(function(n){
                      // ignore empty text nodes
                      return !(n.nodeType === 3 && !n.textContent.trim());
                    });
                    if (children.length === 0) {
                      updatePlaceholderVisibility();
                      return;
                    }
                    children.forEach(function(node){
                      var html = (node.outerHTML !== undefined) ? node.outerHTML : node.textContent;
                      var blockEl = makeCanvasBlock(html);
                      canvas.appendChild(blockEl);
                    });
                    updatePlaceholderVisibility();
                  }

                  // Initialize Sortable for modules (pull: clone)
                  var modulesSortable = Sortable.create(document.getElementById('modulesGrid'), {
                    group: { name: 'modules', pull: 'clone', put: false },
                    sort: false,
                    animation: 150,
                    onEnd: function (evt) {
                      // modules grid end — clicks are handled separately
                    }
                  });

                  // Initialize Sortable on canvas to accept modules and sort existing blocks
                  var canvasSortable = Sortable.create(canvas, {
                    group: { name: 'modules', pull: false, put: true },
                    animation: 150,
                    ghostClass: 'sortable-ghost',
                    onAdd: function (evt) {
                      // when an item is dragged from modules into canvas, evt.item is the clone element (module-tile)
                      var dragged = evt.item;
                      var module = dragged.getAttribute('data-module');
                      // create a real block with contentHtml
                      var contentHtml = createBlockHtmlByModule(module);
                      var blockEl = makeCanvasBlock(contentHtml);
                      // replace the dragged placeholder with the real block
                      dragged.parentNode.replaceChild(blockEl, dragged);
                      syncCanvasToHtmlField();

                      // If the inserted module is code, open the block-level editor immediately
                      if (module === 'code') {
                        // find the block's body node and open block editor
                        var body = blockEl.querySelector('.code-module-body');
                        if (body) {
                          // small delay to ensure DOM is ready
                          setTimeout(function(){ openBlockEditor(body); }, 50);
                        }
                      }
                    },
                    onUpdate: function(evt){
                      // reorder happened
                      syncCanvasToHtmlField();
                    },
                    onRemove: function(evt){
                      syncCanvasToHtmlField();
                    }
                  });

                  // Also support clicking module tiles to insert at end
                  $all('.module-tile').forEach(function(t){
                    t.addEventListener('click', function(){
                      var module = t.getAttribute('data-module');
                      if (module === 'code') {
                        // open full HTML editor (editing whole message)
                        openHtmlEditor();
                        // defaultSaveHandler already handles full save
                        return;
                      }
                      var html = createBlockHtmlByModule(module);
                      var block = makeCanvasBlock(html);
                      // if placeholder exists, replace it
                      var placeholder = canvas.querySelector('.drag-placeholder');
                      if (placeholder) {
                        placeholder.remove();
                      }
                      canvas.appendChild(block);
                      // ensure block is sortable (Sortable automatically includes it)
                      syncCanvasToHtmlField();
                    });
                  });

                  // Hydrate initial html into canvas
                  loadHtmlIntoCanvas(htmlField.value);

                  // If no HTML and we want a code module shown by default, create it and open editor
                  // (This matches the requested UX: when first inserting code or when campaign empty show code editor option)
                  if ((!htmlField.value || htmlField.value.trim() === '') && canvas.querySelectorAll('.canvas-block').length === 0) {
                    // leave placeholder visible; user will insert modules intentionally
                  }

                  // Keep canvas and htmlField in sync on form submit
                  form.addEventListener('submit', function(){
                    syncCanvasToHtmlField();
                  });

                  // Utility: preview and save as template placeholders (not implemented)
                  var previewBtn = document.getElementById('previewBtn');
                  var saveAsTplBtn = document.getElementById('saveAsTplBtn');
                  if (previewBtn) previewBtn.addEventListener('click', function(){ alert('Preview not implemented in this demo.'); });
                  if (saveAsTplBtn) saveAsTplBtn.addEventListener('click', function(){ alert('Save as template not implemented.'); });

                  // ESC to close html modal
                  document.addEventListener('keydown', function(e){
                    if (e.key === 'Escape') {
                      if (htmlBackdrop && htmlBackdrop.classList.contains('show')) closeHtmlEditor();
                    }
                  });

                  // Accessibility: when dragging from modules, set aria-grabbed (basic)
                                    $all('.module-tile').forEach(function(t){
                    t.setAttribute('draggable', 'true');
                  });

                  // ── Content Quality Indicator ──────────────────────────────────
                  var cqBadge = document.getElementById('cqBadge');
                  var cqTip   = document.getElementById('cqTip');

                  function updateContentQuality() {
                    if (!cqBadge || !cqTip) return;

                    // Collect text and count images from canvas
                    var blocks    = canvas.querySelectorAll('.canvas-block .block-content');
                    var textChars = 0;
                    var imgCount  = 0;

                    blocks.forEach(function(b){
                      // Count img tags
                      imgCount += b.querySelectorAll('img').length;
                      // Count visible text characters (strip tags)
                      var tmp = document.createElement('div');
                      tmp.innerHTML = b.innerHTML;
                      var t = (tmp.innerText || tmp.textContent || '').trim();
                      textChars += t.length;
                    });

                    var hasImages = imgCount > 0;

                    if (!hasImages && textChars === 0) {
                      cqBadge.style.background = '#e3e8f0';
                      cqBadge.style.color = '#6b778c';
                      cqBadge.innerText = 'Empty';
                      cqTip.innerText = 'Add modules to start building your email.';
                    } else if (hasImages && textChars < 300) {
                      cqBadge.style.background = '#ffe5e5';
                      cqBadge.style.color = '#fa5252';
                      cqBadge.innerText = '⚠ Image-only';
                      cqTip.innerText = 'Too little text — add a Text block. Image-only emails are flagged as spam.';
                    } else if (hasImages && textChars < 800) {
                      cqBadge.style.background = '#fff3cd';
                      cqBadge.style.color = '#b45309';
                      cqBadge.innerText = '⚡ Low text';
                      cqTip.innerText = 'Add more descriptive text to improve deliverability (' + textChars + ' chars, aim for 800+).';
                    } else if (hasImages && textChars < 1600) {
                      cqBadge.style.background = '#fff3cd';
                      cqBadge.style.color = '#d97706';
                      cqBadge.innerText = '✓ Fair';
                      cqTip.innerText = 'Acceptable balance, but more text will improve inbox placement (' + textChars + ' chars).';
                    } else {
                      cqBadge.style.background = '#d3f9e9';
                      cqBadge.style.color = '#12b886';
                      cqBadge.innerText = '✓ Good';
                      cqTip.innerText = 'Text-to-image balance is good (' + textChars + ' chars).';
                    }
                  }

                  // Observe canvas mutations to update indicator live
                  var cqObserver = new MutationObserver(function(){ updateContentQuality(); });
                  cqObserver.observe(canvas, { childList: true, subtree: true, characterData: true });
                  // Initial run after hydration
                  setTimeout(updateContentQuality, 200);

                })();
              </script>

              <!-- Global toast element (used by autosave / success / error messages) -->
              <div id="globalToast" class="toast" role="status" aria-live="polite" aria-atomic="true"></div>

            <?php } // End campaign check ?>
            <?php } // End PDO check ?>

          <?php elseif ($page === 'review'): ?>
            <?php
              if ($pdo === null) {
                  echo '<div class="page-title">Database Error</div>';
                  echo '<div class="card"><div style="padding: 20px;">Database connection not available. Please check your configuration.</div></div>';
              } else {
                  $id = (int)($_GET['id'] ?? 0);
                  $campaign = get_campaign($pdo, $id);
                  if (!$campaign) {
                    echo "<p>Campaign not found.</p>";
                  } else {
                    // Prepare preview HTML (raw)
                    $previewHtml = $campaign['html'] ?: '<div style="padding:24px;color:#6B778C;">(No content yet — add modules in the Editor)</div>';
                    if (!empty($campaign['unsubscribe_enabled'])) {
                    $basePreviewUrl = get_base_url() . '?t=unsubscribe&cid=' . (int)$campaign['id'];
                    $previewHtml .= '<div style="text-align:center;margin-top:20px;color:#1F2933;font-size:13px;">';
                    $previewHtml .= '<a href="' . $basePreviewUrl . '" style="color:#1A82E2;margin-right:8px;">Unsubscribe</a>';
                    $previewHtml .= '<span style="color:#6B778C;">-</span>';
                    $previewHtml .= '<a href="' . $basePreviewUrl . '" style="color:#1A82E2;margin-left:8px;">Unsubscribe Preferences</a>';
                    $previewHtml .= '</div>';
                }
                $testSent = isset($_GET['test_sent']);
                $sentFlag = isset($_GET['sent']);
                $sendOk = isset($_GET['send_ok']);
                $sendErr = isset($_GET['send_err']);
            ?>
              <div class="page-title">Review &amp; Send — <?php echo h($campaign['name']); ?></div>
              <div class="page-subtitle">Confirm your envelope settings and preview the content before sending.</div>

              <div class="review-grid">
                <div>
                  <div class="review-summary">
                    <form method="post" id="sendForm">
                      <input type="hidden" name="action" value="send_campaign">
                      <input type="hidden" name="id" value="<?php echo (int)$campaign['id']; ?>">

                      <div class="form-group">
                        <label>Single Send Name</label>
                        <input type="text" name="name" value="<?php echo h($campaign['name']); ?>" disabled>
                      </div>

                      <div class="form-group">
                        <label>From Sender</label>
                        <select name="from_email">
                          <option value=""><?php echo $campaign['from_email'] ? h($campaign['from_email']) : 'Select sender'; ?></option>
                          <?php foreach ($profiles as $p): ?>
                            <option value="<?php echo h($p['from_email']); ?>" <?php if ($campaign['from_email'] === $p['from_email']) echo 'selected'; ?>>
                              <?php echo h($p['from_email'] . ' — ' . $p['profile_name']); ?>
                            </option>
                          <?php endforeach; ?>
                        </select>
                      </div>

                      <div class="form-group">
                        <label>Subject</label>
                        <input type="text" name="subject" value="<?php echo h($campaign['subject']); ?>">
                      </div>

                      <div class="form-group">
                        <label>Preheader</label>
                        <input type="text" name="preheader" value="<?php echo h($campaign['preheader']); ?>">
                      </div>

                      <div class="form-group">
                        <label>Unsubscribe Group</label>
                        <div style="display:flex; align-items:center; gap:10px;">
                          <select name="unsubscribe_group" disabled>
                            <option value="">Default</option>
                          </select>
                          <div class="checkbox-row" style="margin:0;">
                            <input type="checkbox" id="rv_unsub" name="unsubscribe_enabled" <?php if (!empty($campaign['unsubscribe_enabled'])) echo 'checked'; ?>>
                            <label for="rv_unsub" style="font-weight:600;margin:0;">Enable Unsubscribe</label>
                          </div>
                          <div class="checkbox-row" style="margin:0;">
                            <input type="checkbox" id="rv_preheader" name="preheader_injection_enabled" <?php if ($campaign['preheader_injection_enabled'] ?? 1) echo 'checked'; ?>>
                            <label for="rv_preheader" style="font-weight:600;margin:0;">Enable Preheader Ref</label>
                          </div>
                          <div class="checkbox-row" style="margin:0;">
                            <input type="checkbox" id="rv_footer" name="footer_injection_enabled" <?php if ($campaign['footer_injection_enabled'] ?? 1) echo 'checked'; ?>>
                            <label for="rv_footer" style="font-weight:600;margin:0;">Enable Footer Ref</label>
                          </div>
                        </div>
                      </div>

                      <div class="form-group">
                        <label>Schedule</label>
                        <input type="text" value="Send Immediately" disabled>
                      </div>

                      <div class="form-group">
                        <label>Send To Recipients</label>
                        <div class="tag-select-container">
                          <div class="tag-select-display" id="tagSelectDisplay">
                            <span class="tag-select-placeholder">Click to select lists...</span>
                          </div>
                          <div class="tag-select-dropdown" id="tagSelectDropdown">
                            <?php foreach ($contactLists as $cl): 
                              $val = 'list:'.$cl['id'];
                            ?>
                              <div class="tag-select-option" data-value="<?php echo h($val); ?>" data-name="<?php echo h($cl['name']); ?>" data-count="<?php echo (int)$cl['contact_count']; ?>">
                                <div class="tag-select-option-name"><?php echo h($cl['name']); ?></div>
                                <div class="tag-select-option-count"><?php echo number_format((int)$cl['contact_count']); ?> contacts</div>
                              </div>
                            <?php endforeach; ?>
                            <div class="tag-select-option" data-value="manual" data-name="Manual - Enter emails">
                              <div class="tag-select-option-name">Manual - Enter emails</div>
                              <div class="tag-select-option-count">Enter email addresses manually</div>
                            </div>
                          </div>
                          <!-- Hidden inputs to submit selected values -->
                          <div id="hiddenInputsContainer"></div>
                        </div>
                        <small style="display:block; margin-top:4px; color:var(--sg-muted);">Click to select multiple lists</small>
                      </div>

                      <div id="manualRecipientsWrap" style="display:none; margin-top:10px;">
                        <label style="font-weight:600; display:block; margin-bottom:6px;">Manual recipients (one per line)</label>
                        <textarea name="test_recipients" placeholder="test1@example.com&#10;test2@example.com" rows="6" style="width:100%;"></textarea>
                      </div>

                      <div style="display:flex; justify-content:flex-end; gap:8px; margin-top:12px;">
                        <a href="?page=editor&id=<?php echo (int)$campaign['id']; ?>" class="btn btn-outline">Back to Editor</a>
                        <button type="submit" class="btn btn-primary" id="sendBtn">
                          <span id="sendBtnText">Send Immediately</span>
                          <span id="sendBtnLoader" style="display:none;">
                            <span style="display:inline-block; width:12px; height:12px; border:2px solid rgba(255,255,255,0.3); border-top-color:#fff; border-radius:50%; animation:spin 0.8s linear infinite;"></span>
                            Sending...
                          </span>
                        </button>
                      </div>
                    </form>

                    <div style="margin-top:18px;">
                      <div style="font-weight:600; margin-bottom:8px;">Test Your Email</div>
                      <div style="color:var(--sg-muted); margin-bottom:8px;">Test your email before sending to your recipients.</div>

                      <form method="post" style="margin-top:12px;" id="testForm">
                        <input type="hidden" name="action" value="send_test_message">
                        <input type="hidden" name="id" value="<?php echo (int)$campaign['id']; ?>">
                        <div>
                          <label style="display:block; font-weight:600; margin-bottom:6px;">Send a Test Email</label>
                          <input type="text" name="test_addresses" placeholder="Enter up to 10 email addresses, separated by commas" style="width:100%; padding:10px;">
                        </div>
                        <div style="display:flex; justify-content:space-between; align-items:center; margin-top:10px;">
                          <div style="color:var(--sg-muted); font-size:12px;">You can send test messages to up to 10 addresses.</div>
                          <button class="btn btn-primary" type="submit" id="testBtn">
                            <span id="testBtnText">Send Test Message</span>
                            <span id="testBtnLoader" style="display:none;">
                              <span style="display:inline-block; width:12px; height:12px; border:2px solid rgba(255,255,255,0.3); border-top-color:#fff; border-radius:50%; animation:spin 0.8s linear infinite;"></span>
                              Sending...
                            </span>
                          </button>
                        </div>
                      </form>

                      <small class="hint">Test messages are sent immediately and recorded as test events (they won't change campaign status).</small>
                    </div>

                  </div>
                </div>

                <div>
                  <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
                    <div style="font-weight:600;">Content preview</div>
                    <div class="preview-toggle-buttons">
                      <button type="button" class="preview-toggle-btn active" data-mode="desktop">
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
                        Desktop
                      </button>
                      <button type="button" class="preview-toggle-btn" data-mode="mobile">
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2"/><line x1="12" y1="18" x2="12" y2="18"/></svg>
                        Mobile
                      </button>
                      <button type="button" class="preview-toggle-btn" data-mode="plain">
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
                        Plain Text
                      </button>
                    </div>
                  </div>
                  <div class="preview-box" id="previewBox" data-preview-mode="desktop">
                    <?php echo $previewHtml; ?>
                  </div>
                </div>
              </div>

              <script>
                (function(){
                  // ENHANCEMENT: Add loading indicators for form submissions
                  var sendForm = document.getElementById('sendForm');
                  var testForm = document.getElementById('testForm');
                  
                  if (sendForm) {
                    sendForm.addEventListener('submit', function(){
                      var btn = document.getElementById('sendBtn');
                      var text = document.getElementById('sendBtnText');
                      var loader = document.getElementById('sendBtnLoader');
                      if (btn && text && loader) {
                        btn.disabled = true;
                        text.style.display = 'none';
                        loader.style.display = '';
                      }
                    });
                  }
                  
                  if (testForm) {
                    testForm.addEventListener('submit', function(){
                      var btn = document.getElementById('testBtn');
                      var text = document.getElementById('testBtnText');
                      var loader = document.getElementById('testBtnLoader');
                      if (btn && text && loader) {
                        btn.disabled = true;
                        text.style.display = 'none';
                        loader.style.display = '';
                      }
                    });
                  }
                  
                  // TAG-BASED MULTI-SELECT FOR RECIPIENTS
                  var tagSelectDisplay = document.getElementById('tagSelectDisplay');
                  var tagSelectDropdown = document.getElementById('tagSelectDropdown');
                  var hiddenInputsContainer = document.getElementById('hiddenInputsContainer');
                  var manualWrap = document.getElementById('manualRecipientsWrap');
                  var selectedValues = [];
                  
                  function updateTagDisplay() {
                    // Clear display
                    tagSelectDisplay.innerHTML = '';
                    
                    // Set the tag count attribute for dynamic sizing
                    tagSelectDisplay.setAttribute('data-tag-count', selectedValues.length);
                    
                    if (selectedValues.length === 0) {
                      var placeholder = document.createElement('span');
                      placeholder.className = 'tag-select-placeholder';
                      placeholder.textContent = 'Click to select lists...';
                      tagSelectDisplay.appendChild(placeholder);
                      tagSelectDisplay.removeAttribute('data-tag-count');
                    } else {
                      selectedValues.forEach(function(item) {
                        var tag = document.createElement('div');
                        tag.className = 'selected-tag';
                        tag.innerHTML = item.name;
                        
                        var removeBtn = document.createElement('button');
                        removeBtn.className = 'selected-tag-remove';
                        removeBtn.innerHTML = '×';
                        removeBtn.type = 'button';
                        removeBtn.onclick = function(e) {
                          e.stopPropagation();
                          removeSelection(item.value);
                        };
                        
                        tag.appendChild(removeBtn);
                        tagSelectDisplay.appendChild(tag);
                      });
                    }
                    
                    // Update hidden inputs for form submission
                    hiddenInputsContainer.innerHTML = '';
                    selectedValues.forEach(function(item) {
                      var input = document.createElement('input');
                      input.type = 'hidden';
                      input.name = 'audience_select[]';
                      input.value = item.value;
                      hiddenInputsContainer.appendChild(input);
                    });
                    
                    // Update dropdown option states
                    var options = tagSelectDropdown.querySelectorAll('.tag-select-option');
                    options.forEach(function(opt) {
                      var value = opt.getAttribute('data-value');
                      if (selectedValues.some(function(item) { return item.value === value; })) {
                        opt.classList.add('selected');
                      } else {
                        opt.classList.remove('selected');
                      }
                    });
                    
                    // Update manual recipients visibility
                    updateManualVisibility();
                  }
                  
                  function addSelection(value, name) {
                    // Check if not already selected
                    if (!selectedValues.some(function(item) { return item.value === value; })) {
                      selectedValues.push({ value: value, name: name });
                      updateTagDisplay();
                    }
                  }
                  
                  function removeSelection(value) {
                    selectedValues = selectedValues.filter(function(item) { return item.value !== value; });
                    updateTagDisplay();
                  }
                  
                  function updateManualVisibility() {
                    if (manualWrap) {
                      var hasManual = selectedValues.some(function(item) { return item.value === 'manual'; });
                      if (hasManual) {
                        manualWrap.style.display = '';
                      } else {
                        manualWrap.style.display = 'none';
                      }
                    }
                  }
                  
                  // Toggle dropdown
                  if (tagSelectDisplay) {
                    tagSelectDisplay.addEventListener('click', function(e) {
                      e.stopPropagation();
                      tagSelectDropdown.classList.toggle('open');
                      tagSelectDisplay.classList.toggle('open');
                    });
                  }
                  
                  // Handle option clicks
                  if (tagSelectDropdown) {
                    var options = tagSelectDropdown.querySelectorAll('.tag-select-option');
                    options.forEach(function(option) {
                      option.addEventListener('click', function(e) {
                        e.stopPropagation();
                        var value = option.getAttribute('data-value');
                        var name = option.getAttribute('data-name');
                        
                        // Toggle selection
                        if (selectedValues.some(function(item) { return item.value === value; })) {
                          removeSelection(value);
                        } else {
                          addSelection(value, name);
                        }
                      });
                    });
                  }
                  
                  // Close dropdown when clicking outside
                  document.addEventListener('click', function(e) {
                    if (tagSelectDropdown && tagSelectDisplay) {
                      if (!tagSelectDropdown.contains(e.target) && !tagSelectDisplay.contains(e.target)) {
                        tagSelectDropdown.classList.remove('open');
                        tagSelectDisplay.classList.remove('open');
                      }
                    }
                  });
                  
                  // Initialize display
                  updateTagDisplay();

                  // PREVIEW MODE TOGGLE
                  var previewBox = document.getElementById('previewBox');
                  var previewToggleBtns = document.querySelectorAll('.preview-toggle-btn');
                  
                  previewToggleBtns.forEach(function(btn) {
                    btn.addEventListener('click', function() {
                      var mode = btn.getAttribute('data-mode');
                      
                      // Update active button
                      previewToggleBtns.forEach(function(b) { b.classList.remove('active'); });
                      btn.classList.add('active');
                      
                      // Update preview box mode
                      if (previewBox) {
                        previewBox.setAttribute('data-preview-mode', mode);
                        
                        if (mode === 'plain') {
                          // Store current HTML state before converting to plain text
                          previewBox.dataset.htmlContent = previewBox.innerHTML;
                          // Convert HTML to plain text for plain text mode
                          var tempDiv = document.createElement('div');
                          tempDiv.innerHTML = previewBox.innerHTML;
                          var plainText = tempDiv.innerText || tempDiv.textContent;
                          previewBox.textContent = plainText;
                        } else {
                          // Restore HTML from stored state
                          if (previewBox.dataset.htmlContent) {
                            previewBox.innerHTML = previewBox.dataset.htmlContent;
                            delete previewBox.dataset.htmlContent;
                          }
                        }
                      }
                    });
                  });

                  var rvUnsub = document.getElementById('rv_unsub');
                  if (rvUnsub && previewBox) {
                    rvUnsub.addEventListener('change', function(){
                      // Clear stored HTML content when unsubscribe block changes
                      delete previewBox.dataset.htmlContent;
                      
                      var existing = previewBox.querySelector('.preview-unsubscribe-block');
                      if (rvUnsub.checked) {
                        if (!existing) {
                          var div = document.createElement('div');
                          div.className = 'preview-unsubscribe-block';
                          div.style.textAlign = 'center';
                          div.style.marginTop = '20px';
                          div.innerHTML = '<a href="<?php echo get_base_url() . '?t=unsubscribe&cid=' . (int)$campaign['id']; ?>" style="color:#1A82E2;margin-right:8px;">Unsubscribe</a> - <a href="<?php echo get_base_url() . '?t=unsubscribe&cid=' . (int)$campaign['id']; ?>" style="color:#1A82E2;margin-left:8px;">Unsubscribe Preferences</a>';
                          previewBox.appendChild(div);
                        }
                      } else {
                        if (existing) existing.remove();
                      }
                    });
                  }

                  // Show toast if test was sent or campaign sent flag present
                  <?php if ($testSent): ?>
                    document.addEventListener('DOMContentLoaded', function(){ showToast('Test message sent', 'success'); });
                  <?php endif; ?>
                })();
              </script>

            <?php } // End campaign check ?>
            <?php } // End PDO check ?>

          <?php elseif ($page === 'stats'): ?>
            <?php
              if ($pdo === null) {
                  echo '<div class="page-title">Database Error</div>';
                  echo '<div class="card"><div style="padding: 20px;">Database connection not available. Please check your configuration.</div></div>';
              } else {
                  $id = (int)($_GET['id'] ?? 0);
                  $campaign = get_campaign($pdo, $id);
                  if (!$campaign) {
                    echo "<p>Campaign not found.</p>";
                  } else {
                    $stats = get_campaign_stats($pdo, $id);
                    $delivered = max(1, (int)$stats['delivered']);
                    $openRate  = $delivered ? round(($stats['open'] / $delivered) * 100, 1) : 0;
                $clickRate = $delivered ? round(($stats['click'] / $delivered) * 100, 1) : 0;

                // Per-profile aggregation for this campaign (PHP-side)
                $profilesAll = get_profiles($pdo);
                $profilesMap = [];
                foreach ($profilesAll as $p) $profilesMap[(int)$p['id']] = $p;

                $perProfile = [];
                try {
                    // Use aggregation query instead of loading all events for better performance
                    $stmtp = $pdo->prepare("
                        SELECT 
                            JSON_EXTRACT(details, '$.profile_id') as profile_id,
                            event_type,
                            COUNT(*) as cnt
                        FROM events 
                        WHERE campaign_id = ?
                        AND JSON_EXTRACT(details, '$.profile_id') IS NOT NULL
                        GROUP BY JSON_EXTRACT(details, '$.profile_id'), event_type
                    ");
                    $stmtp->execute([$id]);
                    foreach ($stmtp as $row) {
                        $pid = (int)$row['profile_id'];
                        if ($pid <= 0) continue;
                        if (!isset($perProfile[$pid])) $perProfile[$pid] = ['sent'=>0,'delivered'=>0,'bounces'=>0,'unsubscribes'=>0,'clicks'=>0,'opens'=>0];
                        $etype = $row['event_type'];
                        $cnt = (int)$row['cnt'];
                        // Count sent attempts: delivered + bounce + deferred
                        if (in_array($etype, ['delivered','bounce','deferred'])) $perProfile[$pid]['sent'] += $cnt;
                        if ($etype === 'delivered') $perProfile[$pid]['delivered'] += $cnt;
                        if ($etype === 'bounce') $perProfile[$pid]['bounces'] += $cnt;
                        if ($etype === 'unsubscribe' || $etype === 'skipped_unsubscribe') $perProfile[$pid]['unsubscribes'] += $cnt;
                        if ($etype === 'click') $perProfile[$pid]['clicks'] += $cnt;
                        if ($etype === 'open') $perProfile[$pid]['opens'] += $cnt;
                    }
                } catch (Exception $e) {}
            ?>
              <div class="page-title">Stats — <?php echo h($campaign['name']); ?></div>
              <div class="page-subtitle">Delivery &amp; engagement metrics for your Single Send.</div>

              <div class="card" style="margin-bottom:18px;">
                <div class="card-header">
                  <div>
                    <div class="card-title">Summary</div>
                  </div>
                  <a href="?page=list" class="btn btn-outline">← Back to Single Sends</a>
                </div>
                <div class="form-row">
                  <div class="form-group">
                    <label>Subject</label>
                    <div><?php echo h($campaign['subject']); ?></div>
                  </div>
                  <div class="form-group">
                    <label>Sent at</label>
                    <div><?php echo $campaign['sent_at'] ? h($campaign['sent_at']) : '<span class="hint">Not sent</span>'; ?></div>
                  </div>
                </div>
              </div>

              <!-- Real-time Campaign Progress -->
              <?php 
                $progress = get_campaign_progress($pdo, $id);
                if ($progress['status'] === 'queued' || $progress['status'] === 'sending' || ($campaign['status'] === 'sending' && $progress['total'] > 0)):
              ?>
              <div class="card" style="margin-bottom:18px;" id="progressCard">
                <div class="card-header">
                  <div>
                    <div class="card-title">Campaign Progress</div>
                    <div class="card-subtitle">Real-time sending progress (updates automatically)</div>
                  </div>
                </div>
                <div style="padding:16px;">
                  <div style="margin-bottom:12px; font-size:18px; font-weight:600;">
                    <span id="progressText"><?php echo $progress['sent']; ?> / <?php echo $progress['total']; ?> sent (<?php echo $progress['percentage']; ?>%)</span>
                  </div>
                  <div style="background:#E4E7EB; border-radius:4px; height:24px; overflow:hidden; position:relative;">
                    <div id="progressBar" style="background:linear-gradient(90deg, #1A82E2, #3B9EF3); height:100%; width:<?php echo $progress['percentage']; ?>%; transition:width 0.3s;"></div>
                  </div>
                  <div style="margin-top:8px; color:#6B778C; font-size:13px;" id="progressStatus">Status: <?php echo ucfirst($progress['status']); ?></div>
                </div>
              </div>
              <?php endif; ?>

              <!-- Simplified header stats matching requested layout -->
              <div class="header-stats">
                <div class="stat-item">
                  <div class="stat-num"><?php echo (int)$stats['target']; ?></div>
                  <div class="stat-label">Emails Triggered</div>
                </div>
                <div class="stat-item">
                  <div class="stat-num"><?php echo (int)$stats['delivered']; ?></div>
                  <div class="stat-label">Delivered</div>
                </div>
                <div class="stat-item">
                  <div class="stat-num"><?php echo (int)$stats['open']; ?></div>
                  <div class="stat-label">Unique Opens</div>
                  <a href="?action=download_events&type=open&campaign_id=<?php echo (int)$id; ?>" class="btn-mini" style="margin-top:8px; font-size:10px;">
                    <svg width="12" height="12" viewBox="0 0 20 20" fill="currentColor" style="margin-right:4px; display:inline-block; vertical-align:middle;">
                      <path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"/>
                    </svg>
                    Download
                  </a>
                </div>
                <div class="stat-item">
                  <div class="stat-num"><?php echo (int)$stats['click']; ?></div>
                  <div class="stat-label">Unique Clicks</div>
                  <a href="?action=download_events&type=click&campaign_id=<?php echo (int)$id; ?>" class="btn-mini" style="margin-top:8px; font-size:10px;">
                    <svg width="12" height="12" viewBox="0 0 20 20" fill="currentColor" style="margin-right:4px; display:inline-block; vertical-align:middle;">
                      <path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"/>
                    </svg>
                    Download
                  </a>
                </div>
                <div class="stat-item">
                  <div class="stat-num"><?php echo (int)$stats['bounce']; ?></div>
                  <div class="stat-label">Bounces</div>
                </div>
                <div class="stat-item">
                  <div class="stat-num"><?php echo (int)$stats['unsubscribe']; ?></div>
                  <div class="stat-label">Unsubscribes</div>
                </div>
                <div class="stat-item">
                  <div class="stat-num"><?php echo (int)$stats['spam']; ?></div>
                  <div class="stat-label">Spam Reports</div>
                </div>
              </div>

              <!-- Campaign Statistics Over Time Chart -->
              <div class="card" style="margin-bottom:18px;">
                <div class="card-header">
                  <div>
                    <div class="card-title">Campaign Statistics Over Time</div>
                    <div class="card-subtitle">Email delivery and engagement metrics by day for this campaign</div>
                  </div>
                </div>
                <div style="padding:24px;">
                  <canvas id="campaignStatsChart" style="max-height:320px;"></canvas>
                </div>
              </div>

              <div class="card" style="margin-bottom:18px;">
                <div class="card-header">
                  <div>
                    <div class="card-title">By Sending Profile (this campaign)</div>
                    <div class="card-subtitle">Counts of sends, delivered, bounces and unsubscribes per profile for this campaign.</div>
                  </div>
                </div>
                <table class="table">
                  <thead>
                    <tr>
                      <th>Profile</th>
                      <th>From Email</th>
                      <th>Sent Attempts</th>
                      <th>Delivered</th>
                      <th>Bounces</th>
                      <th>Unsubscribes</th>
                      <th>Opens</th>
                      <th>Clicks</th>
                    </tr>
                  </thead>
                  <tbody>
                    <?php if (empty($perProfile)): ?>
                      <tr>
                        <td colspan="8" style="text-align:center; padding:16px; color:var(--sg-muted);">No per-profile events found for this campaign.</td>
                      </tr>
                    <?php else: ?>
                      <?php foreach ($perProfile as $pid => $vals):
                        $pf = $profilesMap[$pid] ?? ['profile_name'=>'Profile #'.$pid,'from_email'=>''];
                      ?>
                        <tr>
                          <td><?php echo h($pf['profile_name']); ?></td>
                          <td><?php echo h($pf['from_email']); ?></td>
                          <td><?php echo (int)$vals['sent']; ?></td>
                          <td><?php echo (int)$vals['delivered']; ?></td>
                          <td><?php echo (int)$vals['bounces']; ?></td>
                          <td><?php echo (int)$vals['unsubscribes']; ?></td>
                          <td><?php echo (int)$vals['opens']; ?></td>
                          <td><?php echo (int)$vals['clicks']; ?></td>
                        </tr>
                      <?php endforeach; ?>
                    <?php endif; ?>
                  </tbody>
                </table>
              </div>

              <div class="card">
                <div class="card-header">
                  <div>
                    <div class="card-title">Raw Events</div>
                    <div class="card-subtitle">Last 100 events (for performance). Search by email to filter.</div>
                  </div>
                </div>
                
                <!-- Email search form -->
                <div style="padding:16px; border-bottom:1px solid var(--sg-border);">
                  <form method="get" style="display:flex; gap:8px; align-items:center;">
                    <input type="hidden" name="page" value="stats">
                    <input type="hidden" name="id" value="<?php echo (int)$id; ?>">
                    <input type="text" name="search_email" placeholder="Search by email address..." 
                           value="<?php echo isset($_GET['search_email']) ? h($_GET['search_email']) : ''; ?>"
                           style="flex:1; padding:8px 12px; border:1px solid var(--sg-border); border-radius:4px;">
                    <button type="submit" class="btn" style="white-space:nowrap;">Search</button>
                    <?php if (isset($_GET['search_email']) && $_GET['search_email'] !== ''): ?>
                      <a href="?page=stats&id=<?php echo (int)$id; ?>" class="btn btn-outline" style="white-space:nowrap;">Clear</a>
                    <?php endif; ?>
                  </form>
                </div>
                
                <table class="table">
                  <thead>
                    <tr>
                      <th>Time</th>
                      <th>Type</th>
                      <th>Email</th>
                      <th>Device</th>
                      <th>Location</th>
                      <th>IP Address</th>
                      <th>URL</th>
                    </tr>
                  </thead>
                  <tbody>
                    <?php
                      $searchEmail = isset($_GET['search_email']) ? trim($_GET['search_email']) : '';
                      if ($searchEmail !== '') {
                        // Search for specific email in details JSON
                        $escapedSearch = escape_like_pattern($searchEmail);
                        $stmt = $pdo->prepare("SELECT id, event_type, created_at, details FROM events WHERE campaign_id=? AND JSON_EXTRACT(details, '$.rcpt') LIKE ? ORDER BY created_at DESC LIMIT 100");
                        $stmt->execute([$id, '%' . $escapedSearch . '%']);
                      } else {
                        // Show last 100 events
                        $stmt = $pdo->prepare("SELECT id, event_type, created_at, details FROM events WHERE campaign_id=? ORDER BY created_at DESC LIMIT 100");
                        $stmt->execute([$id]);
                      }
                      $evs = $stmt->fetchAll(PDO::FETCH_ASSOC);
                      if (empty($evs)):
                    ?>
                      <tr>
                        <td colspan="7" style="text-align:center; padding:16px; color:var(--sg-muted);">
                          <?php echo $searchEmail !== '' ? 'No events found for this email.' : 'No events yet.'; ?>
                        </td>
                      </tr>
                    <?php else: ?>
                      <?php foreach ($evs as $ev):
                        $dt = h($ev['created_at']);
                        $type = strtoupper($ev['event_type']);
                        $detailsJson = json_decode($ev['details'], true);
                        $email = isset($detailsJson['rcpt']) ? h($detailsJson['rcpt']) : '—';
                        $device = isset($detailsJson['device']) ? h($detailsJson['device']) : '—';
                        $country = isset($detailsJson['country']) ? h($detailsJson['country']) : '';
                        $state = isset($detailsJson['state']) ? h($detailsJson['state']) : '';
                        $location = trim($state . ($state && $country ? ', ' : '') . $country);
                        if ($location === '') $location = '—';
                        $ip = isset($detailsJson['ip']) ? h($detailsJson['ip']) : '—';
                        $url = isset($detailsJson['url']) ? h($detailsJson['url']) : '—';
                      ?>
                        <tr>
                          <td style="white-space:nowrap;"><?php echo $dt; ?></td>
                          <td><span style="font-weight:600; color:<?php echo $type === 'CLICK' ? 'var(--sg-blue)' : ($type === 'OPEN' ? '#28A745' : 'var(--sg-muted)'); ?>"><?php echo $type; ?></span></td>
                          <td style="font-size:13px;"><?php echo $email; ?></td>
                          <td style="font-size:12px;"><span style="padding:2px 6px; border-radius:4px; background:#EFF3F9; color:#4B5563; font-weight:500;"><?php echo $device; ?></span></td>
                          <td style="font-size:13px;"><?php echo $location; ?></td>
                          <td style="font-size:12px; font-family:monospace;"><?php echo $ip; ?></td>
                          <td style="font-size:12px; max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="<?php echo $url; ?>"><?php echo $url; ?></td>
                        </tr>
                      <?php endforeach; ?>
                    <?php endif; ?>
                  </tbody>
                </table>
              </div>

              <!-- OPTIMIZED: Real-time Progress JavaScript with adaptive polling -->
              <script>
                (function(){
                  var campaignId = <?php echo (int)$id; ?>;
                  var progressCard = document.getElementById('progressCard');
                  var progressBar = document.getElementById('progressBar');
                  var progressText = document.getElementById('progressText');
                  var progressStatus = document.getElementById('progressStatus');
                  
                  if (progressCard) {
                    var lastPercentage = 0;
                    var consecutiveUnchanged = 0;
                    var pollIntervalId = null;
                    var currentPollInterval = 2000;
                    
                    var pollProgress = function() {
                      fetch('?api=progress&campaign_id=' + campaignId)
                        .then(function(r){ return r.json(); })
                        .then(function(data){
                          if (data.success && data.progress) {
                            var prog = data.progress;
                            var stats = data.stats || {};
                            
                            // Track changes for adaptive polling
                            if (prog.percentage === lastPercentage) {
                              consecutiveUnchanged++;
                            } else {
                              consecutiveUnchanged = 0;
                              lastPercentage = prog.percentage;
                              // Speed back up to fast polling when progress resumes
                              if (currentPollInterval !== 2000 && pollIntervalId) {
                                currentPollInterval = 2000;
                                clearInterval(pollIntervalId);
                                pollIntervalId = setInterval(pollProgress, 2000);
                              }
                            }
                            
                            // PERFORMANCE: Batch DOM updates with requestAnimationFrame
                            requestAnimationFrame(function(){
                              // Update progress bar
                              if (progressBar) {
                                progressBar.style.width = prog.percentage + '%';
                              }
                              
                              // Update progress text
                              if (progressText) {
                                progressText.textContent = prog.sent + ' / ' + prog.total + ' sent (' + prog.percentage + '%)';
                              }
                              
                              // Update status
                              if (progressStatus) {
                                progressStatus.textContent = 'Status: ' + prog.status.charAt(0).toUpperCase() + prog.status.slice(1);
                              }
                              
                              // Update stats in header
                              var statItems = document.querySelectorAll('.stat-item .stat-num');
                              if (statItems.length >= 7) {
                                // statItems[0] is Emails Triggered (don't update)
                                if (statItems[1]) statItems[1].textContent = stats.delivered || 0;
                                if (statItems[2]) statItems[2].textContent = stats.open || 0;
                                if (statItems[3]) statItems[3].textContent = stats.click || 0;
                                if (statItems[4]) statItems[4].textContent = stats.bounce || 0;
                                if (statItems[5]) statItems[5].textContent = stats.unsubscribe || 0;
                                // statItems[6] is Spam Reports (don't update from this API)
                              }
                            });
                            
                            // Stop polling and reload page if campaign is completed
                            if (prog.status === 'completed' || prog.campaign_status === 'sent') {
                              clearInterval(pollIntervalId);
                              // Reload page after 2 seconds to show final "sent" status
                              setTimeout(function(){
                                window.location.reload();
                              }, 2000);
                            }
                            
                            // PERFORMANCE: Adaptive polling - slow down if no changes
                            if (consecutiveUnchanged >= 3 && currentPollInterval !== 5000 && pollIntervalId) {
                              currentPollInterval = 5000;
                              clearInterval(pollIntervalId);
                              // Slower polling (every 5 seconds) when stagnant
                              pollIntervalId = setInterval(pollProgress, 5000);
                            }
                            }
                          }
                        })
                        .catch(function(err){
                          console.error('Progress poll error:', err);
                          // On error (e.g. proxy disconnect), reset to fast polling so recovery is quick
                          if (currentPollInterval !== 2000 && pollIntervalId) {
                            currentPollInterval = 2000;
                            consecutiveUnchanged = 0;
                            clearInterval(pollIntervalId);
                            pollIntervalId = setInterval(pollProgress, 2000);
                          }
                        });
                    };
                    
                    // Start polling every 2 seconds
                    pollIntervalId = setInterval(pollProgress, 2000);
                    // Initial poll immediately
                    pollProgress();
                  }
                })();
              </script>

              <script>
              (function() {
                // Initialize campaign stats chart
                const ctx = document.getElementById('campaignStatsChart');
                if (!ctx) return;
                
                const campaignId = <?php echo (int)$id; ?>;
                
                // Fetch chart data for this campaign
                fetch(window.location.pathname + '?ajax=chart_data&id=' + encodeURIComponent(campaignId) + '&days=<?php echo DEFAULT_CHART_DAYS; ?>')
                  .then(response => response.json())
                  .then(data => {
                    // Prepare data for Chart.js
                    const labels = data.map(d => d.date);
                    const delivered = data.map(d => d.delivered);
                    const opens = data.map(d => d.open);
                    const clicks = data.map(d => d.click);
                    const bounces = data.map(d => d.bounce);
                    const unsubscribes = data.map(d => d.unsubscribe);
                    const spam = data.map(d => d.spam);
                    
                    // Create chart
                    new Chart(ctx, {
                      type: 'line',
                      data: {
                        labels: labels,
                        datasets: [
                          {
                            label: 'Delivered',
                            data: delivered,
                            borderColor: '#28A745',
                            backgroundColor: 'rgba(40, 167, 69, 0.1)',
                            borderWidth: 2,
                            fill: true,
                            tension: 0.4
                          },
                          {
                            label: 'Opens',
                            data: opens,
                            borderColor: '#1A82E2',
                            backgroundColor: 'rgba(26, 130, 226, 0.1)',
                            borderWidth: 2,
                            fill: true,
                            tension: 0.4
                          },
                          {
                            label: 'Clicks',
                            data: clicks,
                            borderColor: '#FAB005',
                            backgroundColor: 'rgba(250, 176, 5, 0.1)',
                            borderWidth: 2,
                            fill: true,
                            tension: 0.4
                          },
                          {
                            label: 'Bounces',
                            data: bounces,
                            borderColor: '#FA5252',
                            backgroundColor: 'rgba(250, 82, 82, 0.1)',
                            borderWidth: 2,
                            fill: true,
                            tension: 0.4
                          },
                          {
                            label: 'Unsubscribes',
                            data: unsubscribes,
                            borderColor: '#868E96',
                            backgroundColor: 'rgba(134, 142, 150, 0.1)',
                            borderWidth: 2,
                            fill: true,
                            tension: 0.4
                          },
                          {
                            label: 'Spam Reports',
                            data: spam,
                            borderColor: '#E03131',
                            backgroundColor: 'rgba(224, 49, 49, 0.1)',
                            borderWidth: 2,
                            fill: true,
                            tension: 0.4
                          }
                        ]
                      },
                      options: {
                        responsive: true,
                        maintainAspectRatio: true,
                        aspectRatio: 2.5,
                        plugins: {
                          legend: {
                            position: 'top',
                            labels: {
                              usePointStyle: true,
                              padding: 15,
                              font: {
                                size: 12,
                                family: "'Open Sans', sans-serif"
                              }
                            }
                          },
                          tooltip: {
                            mode: 'index',
                            intersect: false,
                            backgroundColor: 'rgba(0, 0, 0, 0.8)',
                            padding: 12,
                            titleFont: {
                              size: 13,
                              family: "'Open Sans', sans-serif"
                            },
                            bodyFont: {
                              size: 12,
                              family: "'Open Sans', sans-serif"
                            }
                          }
                        },
                        scales: {
                          x: {
                            grid: {
                              display: false
                            },
                            ticks: {
                              font: {
                                size: 11,
                                family: "'Open Sans', sans-serif"
                              }
                            }
                          },
                          y: {
                            beginAtZero: true,
                            grid: {
                              color: 'rgba(0, 0, 0, 0.05)'
                            },
                            ticks: {
                              font: {
                                size: 11,
                                family: "'Open Sans', sans-serif"
                              }
                            }
                          }
                        },
                        interaction: {
                          mode: 'nearest',
                          axis: 'x',
                          intersect: false
                        }
                      }
                    });
                  })
                  .catch(error => {
                    console.error('Error loading campaign stats chart data:', error);
                    ctx.parentElement.innerHTML = '<p style="text-align:center; color:#6B778C; padding:40px;">Failed to load chart data</p>';
                  });
              })();
              </script>

            <?php } // End campaign check ?>
            <?php } // End PDO check for stats ?>

          <?php elseif ($page === 'contacts'): ?>
            <?php
              if ($pdo === null) {
                  echo '<div class="page-title">Database Error</div>';
                  echo '<div class="card"><div style="padding: 20px;">Database connection not available. Please check your configuration.</div></div>';
              } else {
                  $listId   = isset($_GET['list_id']) ? (int)$_GET['list_id'] : 0;
                  $showCreate = isset($_GET['show_create']);
            ?>

            <?php if ($listId === 0): ?>
              <?php
                $lists = get_contact_lists($pdo);
              ?>
              <div class="page-title">Contact Lists</div>
              <div class="page-subtitle">Manage your marketing lists and upload contacts</div>

              <div class="card">
                <div class="card-header">
                  <div>
                    <div class="card-title">All Lists</div>
                    <div class="card-subtitle">Create and manage contact lists for your campaigns</div>
                  </div>
                  <div style="display:flex; gap:12px;">
                    <a href="?page=contacts&show_create=1" class="btn btn-primary">
                      <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor" style="margin-right:6px; display:inline-block; vertical-align:middle;">
                        <path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/>
                      </svg>
                      Create New List
                    </a>
                    <?php if (!empty($lists)): ?>
                      <a href="?page=contacts&list_id=<?php echo (int)$lists[0]['id']; ?>" class="btn btn-outline">Add Contacts</a>
                    <?php endif; ?>
                  </div>
                </div>

                <?php if (empty($lists)): ?>
                  <div style="padding:60px 20px; text-align:center;">
                    <div style="font-size:48px; margin-bottom:16px; opacity:0.3;">📋</div>
                    <div style="font-size:18px; font-weight:600; color:var(--sg-text); margin-bottom:8px;">No Contact Lists Yet</div>
                    <div style="font-size:14px; color:var(--sg-muted); margin-bottom:24px;">Create your first list to start organizing your contacts</div>
                    <a href="?page=contacts&show_create=1" class="btn btn-primary">Create Your First List</a>
                  </div>
                <?php else: ?>
                  <table class="table">
                    <thead>
                      <tr>
                        <th>List Name</th>
                        <th>Type</th>
                        <th>Contacts</th>
                        <th>Created</th>
                        <th style="text-align:right;">Actions</th>
                      </tr>
                    </thead>
                    <tbody>
                      <?php foreach ($lists as $l): ?>
                        <tr style="cursor:pointer;" onclick="window.location='?page=contacts&list_id=<?php echo (int)$l['id']; ?>'">
                          <td>
                            <div style="font-weight:600; color:var(--sg-text);"><?php echo h($l['name']); ?></div>
                          </td>
                          <td>
                            <span style="display:inline-block; padding:4px 10px; border-radius:12px; font-size:11px; font-weight:600; background:#E9F3FF; color:var(--sg-blue);">
                              <?php echo strtoupper(h($l['type'])); ?>
                            </span>
                          </td>
                          <td>
                            <span style="font-weight:600; color:var(--sg-blue);"><?php echo number_format((int)$l['contact_count']); ?></span> contacts
                          </td>
                          <td style="color:var(--sg-muted); font-size:13px;"><?php echo h($l['created_at']); ?></td>
                          <td style="text-align:right;">
                            <a class="btn-mini" href="?page=contacts&list_id=<?php echo (int)$l['id']; ?>" onclick="event.stopPropagation();">Manage</a>
                            <form method="post" style="display:inline;" onclick="event.stopPropagation();">
                              <input type="hidden" name="action" value="delete_contact_list">
                              <input type="hidden" name="list_id" value="<?php echo (int)$l['id']; ?>">
                              <button type="submit" class="btn-mini" style="color:var(--sg-danger);" onclick="return confirm('Delete this list and all its contacts?');">Delete</button>
                            </form>
                          </td>
                        </tr>
                      <?php endforeach; ?>
                    </tbody>
                  </table>
                <?php endif; ?>
              </div>

              <?php if ($showCreate): ?>
                <div class="modal-backdrop" id="createListModal">
                  <div class="modal-panel">
                    <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
                      <div style="font-weight:600;">Create Contact List</div>
                      <button class="btn btn-outline" onclick="document.getElementById('createListModal').style.display='none'">✕</button>
                    </div>
                    <form method="post">
                      <input type="hidden" name="action" value="create_contact_list">
                      <div class="form-group">
                        <label>List name</label>
                        <input type="text" name="list_name" required>
                      </div>
                      <div style="display:flex; gap:8px; justify-content:flex-end;">
                        <a href="?page=contacts" class="btn btn-outline">Cancel</a>
                        <button class="btn btn-primary" type="submit">Create</button>
                      </div>
                    </form>
                  </div>
                </div>
                <script>document.getElementById('createListModal').style.display='flex';</script>
              <?php endif; ?>

            <?php else: ?>
              <?php
                $list = get_contact_list($pdo, $listId);
                if (!$list) {
                  echo "<p>List not found.</p>";
                } else {
                  $contacts = get_contacts_for_list($pdo, $listId);
              ?>
                <div class="page-title">
                  <span style="color:var(--sg-muted); font-weight:400;">Contacts /</span> <?php echo h($list['name']); ?>
                </div>
                <div class="page-subtitle">Add contacts manually or upload multiple CSV files to this list</div>

                <!-- Upload Section with Better UI -->
                <div class="card" style="background:linear-gradient(135deg, #F7F8FA 0%, #ffffff 100%); border:2px dashed var(--sg-border);">
                  <div style="padding:32px; text-align:center;">
                    <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="var(--sg-blue)" stroke-width="2" style="margin:0 auto 16px;">
                      <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                      <polyline points="17 8 12 3 7 8"></polyline>
                      <line x1="12" y1="3" x2="12" y2="15"></line>
                    </svg>
                    <div style="font-size:18px; font-weight:600; color:var(--sg-text); margin-bottom:8px;">Upload Contact Files</div>
                    <div style="font-size:14px; color:var(--sg-muted); margin-bottom:24px;">
                      Select one or more CSV files to upload. Each file should contain an 'email' column.
                    </div>
                    
                    <form method="post" enctype="multipart/form-data" id="csvUploadForm">
                      <input type="hidden" name="action" value="upload_contacts_csv">
                      <input type="hidden" name="list_id" value="<?php echo (int)$listId; ?>">
                      <div style="margin-bottom:20px;">
                        <input type="file" name="csv_files[]" id="csvFiles" accept=".csv,text/csv" multiple 
                               style="display:none;" onchange="updateFileList()">
                        <label for="csvFiles" class="btn btn-primary" style="cursor:pointer; display:inline-flex; align-items:center; gap:8px;">
                          <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
                            <path d="M5.5 13a3.5 3.5 0 01-.369-6.98 4 4 0 117.753-1.977A4.5 4.5 0 1113.5 13H11V9.413l1.293 1.293a1 1 0 001.414-1.414l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L9 9.414V13H5.5z"/>
                          </svg>
                          Choose CSV Files
                        </label>
                      </div>
                      <div id="fileList" style="margin-bottom:16px; color:var(--sg-muted); font-size:14px;"></div>
                      <button type="submit" id="uploadBtn" class="btn btn-outline" style="display:none;">
                        Upload Selected Files
                      </button>
                    </form>
                  </div>
                </div>

                <script>
                function updateFileList() {
                  const input = document.getElementById('csvFiles');
                  const fileList = document.getElementById('fileList');
                  const uploadBtn = document.getElementById('uploadBtn');
                  
                  if (input.files.length > 0) {
                    let html = '<div style="text-align:left; display:inline-block; max-width:600px;">';
                    html += '<div style="font-weight:600; margin-bottom:8px; color:var(--sg-text);">' + input.files.length + ' file(s) selected:</div>';
                    for (let i = 0; i < input.files.length; i++) {
                      html += '<div style="padding:6px 12px; background:white; border:1px solid var(--sg-border); border-radius:4px; margin-bottom:4px; display:flex; align-items:center; gap:8px;">';
                      html += '<svg width="16" height="16" viewBox="0 0 20 20" fill="var(--sg-blue)"><path d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z"/></svg>';
                      html += '<span>' + input.files[i].name + '</span>';
                      html += '</div>';
                    }
                    html += '</div>';
                    fileList.innerHTML = html;
                    uploadBtn.style.display = 'inline-block';
                  } else {
                    fileList.innerHTML = '';
                    uploadBtn.style.display = 'none';
                  }
                }
                </script>

                <!-- Manual Add Contact -->
                <div class="card">
                  <div class="card-header">
                    <div>
                      <div class="card-title">Add Contact Manually</div>
                      <div class="card-subtitle">Add individual contacts one at a time</div>
                    </div>
                    <a href="?page=contacts" class="btn btn-outline">← Back to Lists</a>
                  </div>

                  <form method="post" style="padding:20px; border-bottom:1px solid var(--sg-border);">
                    <input type="hidden" name="action" value="add_contact_manual">
                    <input type="hidden" name="list_id" value="<?php echo (int)$listId; ?>">
                    <div style="display:grid; grid-template-columns:2fr 1fr 1fr; gap:12px; margin-bottom:16px;">
                      <div class="form-group" style="margin:0;">
                        <label style="font-weight:600; margin-bottom:6px; display:block;">Email Address *</label>
                        <input type="email" name="email" required placeholder="contact@example.com" style="width:100%;">
                      </div>
                      <div class="form-group" style="margin:0;">
                        <label style="font-weight:600; margin-bottom:6px; display:block;">First Name</label>
                        <input type="text" name="first_name" placeholder="John" style="width:100%;">
                      </div>
                      <div class="form-group" style="margin:0;">
                        <label style="font-weight:600; margin-bottom:6px; display:block;">Last Name</label>
                        <input type="text" name="last_name" placeholder="Doe" style="width:100%;">
                      </div>
                    </div>
                    <button type="submit" class="btn btn-primary">
                      <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor" style="margin-right:6px; display:inline-block; vertical-align:middle;">
                        <path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/>
                      </svg>
                      Add Contact
                    </button>
                  </form>
                </div>

                <!-- Contacts List -->
                <div class="card">
                  <div class="card-header">
                    <div>
                      <div class="card-title">
                        All Contacts 
                        <span style="display:inline-block; padding:4px 10px; border-radius:12px; font-size:11px; font-weight:600; background:var(--sg-blue); color:white; margin-left:8px;">
                          <?php echo number_format(count($contacts)); ?>
                        </span>
                      </div>
                      <div class="card-subtitle">Manage and search your contact list</div>
                    </div>
                    <div>
                      <form method="get" style="display:flex; gap:8px; align-items:center;">
                        <input type="hidden" name="page" value="contacts">
                        <input type="hidden" name="list_id" value="<?php echo (int)$listId; ?>">
                        <input type="text" name="q" placeholder="Search by email or name..." 
                               value="<?php echo isset($_GET['q']) ? h($_GET['q']) : ''; ?>"
                               style="padding:10px 14px; border:1px solid var(--sg-border); border-radius:6px; width:300px; font-size:14px;">
                        <button class="btn btn-outline" type="submit">
                          <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor" style="margin-right:6px; display:inline-block; vertical-align:middle;">
                            <path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/>
                          </svg>
                          Search
                        </button>
                        <?php if (isset($_GET['q']) && $_GET['q'] !== ''): ?>
                          <a href="?page=contacts&list_id=<?php echo (int)$listId; ?>" class="btn btn-outline">Clear</a>
                        <?php endif; ?>
                      </form>
                    </div>
                  </div>

                  <?php if (empty($contacts)): ?>
                    <div style="padding:60px 20px; text-align:center;">
                      <div style="font-size:48px; margin-bottom:16px; opacity:0.3;">👥</div>
                      <div style="font-size:18px; font-weight:600; color:var(--sg-text); margin-bottom:8px;">No Contacts Yet</div>
                      <div style="font-size:14px; color:var(--sg-muted);">Add contacts manually or upload a CSV file to get started</div>
                    </div>
                  <?php else: ?>
                    <div style="overflow-x:auto;">
                      <table class="table">
                        <thead>
                          <tr>
                            <th style="width:50%;">Email Address</th>
                            <th style="width:25%;">Name</th>
                            <th style="width:25%;">Date Added</th>
                          </tr>
                        </thead>
                        <tbody>
                          <?php foreach ($contacts as $ct): ?>
                            <tr>
                              <td>
                                <div style="display:flex; align-items:center; gap:10px;">
                                  <div style="width:32px; height:32px; border-radius:999px; background:linear-gradient(135deg, var(--sg-blue) 0%, #0D6EFD 100%); color:white; display:flex; align-items:center; justify-content:center; font-weight:600; font-size:12px; flex-shrink:0;">
                                    <?php echo strtoupper(substr($ct['email'], 0, 1)); ?>
                                  </div>
                                  <div style="font-weight:500; color:var(--sg-text);"><?php echo h($ct['email']); ?></div>
                                </div>
                              </td>
                              <td>
                                <span style="color:var(--sg-text);">
                                  <?php 
                                    $name = trim($ct['first_name'] . ' ' . $ct['last_name']);
                                    echo $name !== '' ? h($name) : '<span style="color:var(--sg-muted); font-style:italic;">No name</span>';
                                  ?>
                                </span>
                              </td>
                              <td style="color:var(--sg-muted); font-size:13px;"><?php echo h($ct['created_at']); ?></td>
                            </tr>
                          <?php endforeach; ?>
                        </tbody>
                      </table>
                    </div>
                  <?php endif; ?>
                </div>
              <?php } // End else block for if (!$list) ?>

            <?php endif; ?> <!-- End if ($listId === 0) -->
            <?php } // End if($pdo === null) else block for contacts ?>

          <?php elseif ($page === 'activity'): ?>
            <?php
              if ($pdo === null) {
                  echo '<div class="page-title">Database Error</div>';
                  echo '<div class="card"><div style="padding: 20px;">Database connection not available. Please check your configuration.</div></div>';
              } else {
                  // Activity page showing unified unsubscribes and bounces
                  $searchEmail = isset($_GET['search_email']) ? trim($_GET['search_email']) : '';
                  if ($searchEmail !== '') {
                      // Search for specific email in activity events
                      $escapedSearch = escape_like_pattern($searchEmail);
                      $stmt = $pdo->prepare("SELECT id, event_type, created_at, details FROM events WHERE event_type IN ('unsubscribe','bounce','skipped_unsubscribe','profile_disabled') AND JSON_EXTRACT(details, '$.email') LIKE ? ORDER BY created_at DESC LIMIT 200");
                      $stmt->execute(['%' . $escapedSearch . '%']);
                  } else {
                      $stmt = $pdo->query("SELECT id, event_type, created_at, details FROM events WHERE event_type IN ('unsubscribe','bounce','skipped_unsubscribe','profile_disabled') ORDER BY created_at DESC LIMIT 200");
                  }
                  $acts = $stmt->fetchAll(PDO::FETCH_ASSOC);
            ?>
            <div class="page-title">Activity</div>
            <div class="page-subtitle">Recent unsubscribes, bounces, and profile disables.</div>

            <div class="card">
              <div class="card-header">
                <div>
                  <div class="card-title">Recent Activity</div>
                  <div class="card-subtitle">Click an event to view details (emails, errors). Search by email to filter.</div>
                </div>
                <div>
                  <a href="?page=list" class="btn btn-outline">← Single Sends</a>
                </div>
              </div>

              <!-- Email search form -->
              <div style="padding:16px; border-bottom:1px solid var(--sg-border);">
                <form method="get" style="display:flex; gap:8px; align-items:center;">
                  <input type="hidden" name="page" value="activity">
                  <input type="text" name="search_email" placeholder="Search by email address..." 
                         value="<?php echo isset($_GET['search_email']) ? h($_GET['search_email']) : ''; ?>"
                         style="flex:1; padding:8px 12px; border:1px solid var(--sg-border); border-radius:4px;">
                  <button type="submit" class="btn" style="white-space:nowrap;">Search</button>
                  <?php if (isset($_GET['search_email']) && $_GET['search_email'] !== ''): ?>
                    <a href="?page=activity" class="btn btn-outline" style="white-space:nowrap;">Clear</a>
                  <?php endif; ?>
                </form>
              </div>

              <div style="display:flex; gap:8px; margin-bottom:10px; padding:16px; padding-bottom:0;">
                <form method="post" onsubmit="return confirm('Delete ALL unsubscribes?');">
                  <input type="hidden" name="action" value="delete_unsubscribes">
                  <button class="btn btn-outline" type="submit">Clear Unsubscribes</button>
                </form>
                <form method="post" onsubmit="return confirm('Delete ALL bounce events?');">
                  <input type="hidden" name="action" value="delete_bounces">
                  <button class="btn btn-outline" type="submit">Clear Bounces</button>
                </form>
              </div>

              <?php if (empty($acts)): ?>
                <div class="hint" style="padding:16px;"><?php echo $searchEmail !== '' ? 'No events found for this email.' : 'No recent unsubscribes or bounces.'; ?></div>
              <?php else: ?>
                <table class="table">
                  <thead>
                    <tr>
                      <th>Time</th>
                      <th>Type</th>
                      <th>Summary</th>
                      <th>Details</th>
                    </tr>
                  </thead>
                  <tbody>
                    <?php foreach ($acts as $a): 
                      $details = json_decode($a['details'], true);
                      $summary = '';
                      if ($a['event_type'] === 'bounce') {
                        $summary = isset($details['rcpt']) ? h($details['rcpt']) : 'Bounce';
                      } elseif ($a['event_type'] === 'unsubscribe' || $a['event_type'] === 'skipped_unsubscribe') {
                        $summary = isset($details['rcpt']) ? h($details['rcpt']) : 'Unsubscribe';
                      } elseif ($a['event_type'] === 'profile_disabled') {
                        $summary = 'Profile ' . (isset($details['profile_id']) ? h($details['profile_id']) : '');
                      }
                    ?>
                      <tr>
                        <td><?php echo h($a['created_at']); ?></td>
                        <td><?php echo h(ucfirst($a['event_type'])); ?></td>
                        <td><?php echo $summary; ?></td>
                        <td><code style="font-size:11px;"><?php echo h($a['details']); ?></code></td>
                      </tr>
                    <?php endforeach; ?>
                  </tbody>
                </table>
              <?php endif; ?>
            </div>
            <?php } // End PDO check for activity ?>

          <?php elseif ($page === 'dashboard'): ?>
            <?php
              if ($pdo === null) {
                  echo '<div class="page-title">Database Error</div>';
                  echo '<div class="card"><div style="padding: 20px;">Database connection not available. Please check your configuration.</div></div>';
              } else {
                  // Get all campaigns for dashboard statistics (optimized query)
                  $stmt = $pdo->query("SELECT id, name, subject, status, created_at FROM campaigns ORDER BY created_at DESC");
                  $allCampaigns = $stmt->fetchAll(PDO::FETCH_ASSOC);
                  
                  // Calculate aggregate statistics
                  $totalCampaigns = count($allCampaigns);
                  $totalSent = 0;
                  $totalDelivered = 0;
                  $totalOpens = 0;
                  $totalClicks = 0;
                  $totalBounces = 0;
                  $totalUnsubscribes = 0;
                  
                  // Cache campaign stats to avoid N+1 queries
                  $campaignStats = [];
                  
                  foreach ($allCampaigns as $campaign) {
                      $stats = get_campaign_stats($pdo, $campaign['id']);
                      $campaignStats[$campaign['id']] = $stats;
                      
                      $totalSent += (int)$stats['target'];
                      $totalDelivered += (int)$stats['delivered'];
                      $totalOpens += (int)$stats['open'];
                      $totalClicks += (int)$stats['click'];
                      $totalBounces += (int)$stats['bounce'];
                      $totalUnsubscribes += (int)$stats['unsubscribe'];
                  }
                  
                  // Calculate rates
                  $openRate = $totalDelivered > 0 ? round(($totalOpens / $totalDelivered) * 100, 1) : 0;
                  $clickRate = $totalDelivered > 0 ? round(($totalClicks / $totalDelivered) * 100, 1) : 0;
                  $bounceRate = $totalSent > 0 ? round(($totalBounces / $totalSent) * 100, 1) : 0;
                  
                  // Get recent campaigns (last 10)
                  $recentCampaigns = array_slice($allCampaigns, 0, 10);
            ?>
            
            <div class="page-title">Dashboard</div>
            <div class="page-subtitle">Campaign performance overview and activity statistics</div>

            <!-- Success alert notification if test_sent parameter exists -->
            <?php if (isset($_GET['test_sent']) && $_GET['test_sent'] === '1'): ?>
              <div style="display:flex; align-items:center; gap:12px; padding:14px 18px; margin-bottom:20px; background:#D4EDDA; color:#155724; border:1px solid #C3E6CB; border-radius:6px; box-shadow:0 2px 4px rgba(0,0,0,0.05);">
                <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" style="flex-shrink:0;">
                  <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
                </svg>
                <div style="font-weight:600;">Test email sent successfully</div>
              </div>
            <?php endif; ?>

            <!-- Overall Statistics Cards -->
            <div style="display:grid; grid-template-columns:repeat(5, 1fr); gap:16px; margin-bottom:24px;">
              <div class="card" style="padding:20px; background:linear-gradient(135deg, #fff 0%, #f8f9fa 100%);">
                <div style="color:var(--sg-muted); font-size:13px; margin-bottom:4px; font-weight:600;">Emails Sent</div>
                <div style="font-size:32px; font-weight:700; color:var(--sg-text);"><?php echo number_format($totalSent); ?></div>
              </div>
              
              <div class="card" style="padding:20px; background:linear-gradient(135deg, #e8f5e9 0%, #fff 100%);">
                <div style="color:var(--sg-muted); font-size:13px; margin-bottom:4px; font-weight:600;">Delivered</div>
                <div style="font-size:32px; font-weight:700; color:#28A745;"><?php echo number_format($totalDelivered); ?></div>
                <div style="font-size:11px; color:#28A745; margin-top:4px; font-weight:600;">✓ Successfully Delivered</div>
              </div>
              
              <div class="card" style="padding:20px; background:linear-gradient(135deg, #e3f2fd 0%, #fff 100%);">
                <div style="color:var(--sg-muted); font-size:13px; margin-bottom:4px; font-weight:600;">Open Rate</div>
                <div style="font-size:32px; font-weight:700; color:var(--sg-blue);"><?php echo $openRate; ?>%</div>
                <div style="font-size:11px; color:var(--sg-blue); margin-top:4px; font-weight:600;"><?php echo number_format($totalOpens); ?> opens</div>
              </div>
              
              <div class="card" style="padding:20px; background:linear-gradient(135deg, #e3f2fd 0%, #fff 100%);">
                <div style="color:var(--sg-muted); font-size:13px; margin-bottom:4px; font-weight:600;">Click Rate</div>
                <div style="font-size:32px; font-weight:700; color:var(--sg-blue);"><?php echo $clickRate; ?>%</div>
                <div style="font-size:11px; color:var(--sg-blue); margin-top:4px; font-weight:600;"><?php echo number_format($totalClicks); ?> clicks</div>
              </div>
              
              <div class="card" style="padding:20px; background:linear-gradient(135deg, #ffebee 0%, #fff 100%);">
                <div style="color:var(--sg-muted); font-size:13px; margin-bottom:4px; font-weight:600;">Bounce Rate</div>
                <div style="font-size:32px; font-weight:700; color:var(--sg-danger);"><?php echo $bounceRate; ?>%</div>
                <div style="font-size:11px; color:var(--sg-danger); margin-top:4px; font-weight:600;"><?php echo number_format($totalBounces); ?> bounces</div>
              </div>
            </div>

            <!-- Campaign Statistics Over Time Chart -->
            <div class="card" style="margin-bottom:24px;">
              <div class="card-header">
                <div>
                  <div class="card-title">Campaign Statistics Over Time</div>
                  <div class="card-subtitle">Email delivery and engagement metrics by day (last 30 days)</div>
                </div>
              </div>
              <div style="padding:24px;">
                <canvas id="campaignChart" style="max-height:400px;"></canvas>
              </div>
            </div>

            <!-- Engagement Metrics Card -->
            <div class="card" style="margin-bottom:24px;">
              <div class="card-header">
                <div>
                  <div class="card-title">Engagement Metrics</div>
                </div>
                <div style="display:flex; gap:12px;">
                  <a href="?action=download_events&type=open" class="btn btn-outline" style="white-space:nowrap;">
                    <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor" style="margin-right:6px; display:inline-block; vertical-align:middle;">
                      <path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"/>
                    </svg>
                    Download All Opens
                  </a>
                  <a href="?action=download_events&type=click" class="btn btn-outline" style="white-space:nowrap;">
                    <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor" style="margin-right:6px; display:inline-block; vertical-align:middle;">
                      <path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"/>
                    </svg>
                    Download All Clicks
                  </a>
                </div>
              </div>
              <div style="padding:20px;">
                <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:24px;">
                  <div>
                    <div style="font-size:13px; color:var(--sg-muted); margin-bottom:6px;">Total Campaigns</div>
                    <div style="font-size:24px; font-weight:600;"><?php echo $totalCampaigns; ?></div>
                  </div>
                  <div>
                    <div style="font-size:13px; color:var(--sg-muted); margin-bottom:6px;">Total Opens</div>
                    <div style="font-size:24px; font-weight:600;"><?php echo number_format($totalOpens); ?></div>
                  </div>
                  <div>
                    <div style="font-size:13px; color:var(--sg-muted); margin-bottom:6px;">Total Clicks</div>
                    <div style="font-size:24px; font-weight:600;"><?php echo number_format($totalClicks); ?></div>
                  </div>
                  <div>
                    <div style="font-size:13px; color:var(--sg-muted); margin-bottom:6px;">Total Bounces</div>
                    <div style="font-size:24px; font-weight:600;"><?php echo number_format($totalBounces); ?></div>
                  </div>
                  <div>
                    <div style="font-size:13px; color:var(--sg-muted); margin-bottom:6px;">Unsubscribes</div>
                    <div style="font-size:24px; font-weight:600;"><?php echo number_format($totalUnsubscribes); ?></div>
                  </div>
                </div>
              </div>
            </div>

            <!-- Recent Campaigns Card -->
            <div class="card">
              <div class="card-header">
                <div>
                  <div class="card-title">Recent Campaigns</div>
                  <div class="card-subtitle">Latest campaign activity and status</div>
                </div>
                <a href="?page=list" class="btn btn-primary">View All Campaigns</a>
              </div>
              
              <?php if (empty($recentCampaigns)): ?>
                <div class="hint" style="padding:40px; text-align:center;">
                  <div style="font-size:48px; margin-bottom:12px; opacity:0.3;">📧</div>
                  <div style="font-size:16px; color:var(--sg-muted);">No campaigns yet. Create your first campaign to get started!</div>
                  <div style="margin-top:16px;">
                    <a href="?page=list" class="btn btn-primary">Create Campaign</a>
                  </div>
                </div>
              <?php else: ?>
                <table class="table">
                  <thead>
                    <tr>
                      <th>Campaign Name</th>
                      <th>Status</th>
                      <th>Sent</th>
                      <th>Delivered</th>
                      <th>Opens</th>
                      <th>Clicks</th>
                      <th>Bounces</th>
                      <th>Actions</th>
                    </tr>
                  </thead>
                  <tbody>
                    <?php foreach ($recentCampaigns as $c):
                      // Use cached stats instead of querying again
                      $cStats = $campaignStats[$c['id']] ?? ['target' => 0, 'delivered' => 0, 'open' => 0, 'click' => 0, 'bounce' => 0];
                      $statusColor = 'var(--sg-muted)';
                      $statusText = ucfirst($c['status']);
                      
                      if ($c['status'] === 'sent') {
                        $statusColor = '#28A745';
                      } elseif ($c['status'] === 'sending') {
                        $statusColor = 'var(--sg-blue)';
                      } elseif ($c['status'] === 'draft') {
                        $statusColor = 'var(--sg-muted)';
                      }
                    ?>
                      <tr>
                        <td>
                          <a href="?page=stats&id=<?php echo (int)$c['id']; ?>" style="font-weight:600; color:var(--sg-text);">
                            <?php echo h($c['name']); ?>
                          </a>
                          <div style="font-size:12px; color:var(--sg-muted);"><?php echo h($c['subject']); ?></div>
                        </td>
                        <td>
                          <span style="display:inline-block; padding:4px 10px; border-radius:12px; font-size:12px; font-weight:600; background:<?php echo $statusColor; ?>20; color:<?php echo $statusColor; ?>;">
                            <?php echo $statusText; ?>
                          </span>
                        </td>
                        <td><?php echo number_format((int)$cStats['target']); ?></td>
                        <td><?php echo number_format((int)$cStats['delivered']); ?></td>
                        <td><?php echo number_format((int)$cStats['open']); ?></td>
                        <td><?php echo number_format((int)$cStats['click']); ?></td>
                        <td><?php echo number_format((int)$cStats['bounce']); ?></td>
                        <td>
                          <a href="?page=stats&id=<?php echo (int)$c['id']; ?>" class="btn-mini">View Stats</a>
                        </td>
                      </tr>
                    <?php endforeach; ?>
                  </tbody>
                </table>
              <?php endif; ?>
            </div>
            
            <script>
            (function() {
              // Initialize dashboard chart
              const ctx = document.getElementById('campaignChart');
              if (!ctx) return;
              
              // Fetch chart data
              fetch(window.location.pathname + '?ajax=chart_data&days=<?php echo DEFAULT_CHART_DAYS; ?>')
                .then(response => response.json())
                .then(data => {
                  // Prepare data for Chart.js
                  const labels = data.map(d => d.date);
                  const delivered = data.map(d => d.delivered);
                  const opens = data.map(d => d.open);
                  const clicks = data.map(d => d.click);
                  const bounces = data.map(d => d.bounce);
                  const unsubscribes = data.map(d => d.unsubscribe);
                  const spam = data.map(d => d.spam);
                  
                  // Create chart
                  new Chart(ctx, {
                    type: 'line',
                    data: {
                      labels: labels,
                      datasets: [
                        {
                          label: 'Delivered',
                          data: delivered,
                          borderColor: '#28A745',
                          backgroundColor: 'rgba(40, 167, 69, 0.1)',
                          borderWidth: 2,
                          fill: true,
                          tension: 0.4
                        },
                        {
                          label: 'Opens',
                          data: opens,
                          borderColor: '#1A82E2',
                          backgroundColor: 'rgba(26, 130, 226, 0.1)',
                          borderWidth: 2,
                          fill: true,
                          tension: 0.4
                        },
                        {
                          label: 'Clicks',
                          data: clicks,
                          borderColor: '#FAB005',
                          backgroundColor: 'rgba(250, 176, 5, 0.1)',
                          borderWidth: 2,
                          fill: true,
                          tension: 0.4
                        },
                        {
                          label: 'Bounces',
                          data: bounces,
                          borderColor: '#FA5252',
                          backgroundColor: 'rgba(250, 82, 82, 0.1)',
                          borderWidth: 2,
                          fill: true,
                          tension: 0.4
                        },
                        {
                          label: 'Unsubscribes',
                          data: unsubscribes,
                          borderColor: '#868E96',
                          backgroundColor: 'rgba(134, 142, 150, 0.1)',
                          borderWidth: 2,
                          fill: true,
                          tension: 0.4
                        },
                        {
                          label: 'Spam Reports',
                          data: spam,
                          borderColor: '#E03131',
                          backgroundColor: 'rgba(224, 49, 49, 0.1)',
                          borderWidth: 2,
                          fill: true,
                          tension: 0.4
                        }
                      ]
                    },
                    options: {
                      responsive: true,
                      maintainAspectRatio: true,
                      aspectRatio: 2.5,
                      plugins: {
                        legend: {
                          position: 'top',
                          labels: {
                            usePointStyle: true,
                            padding: 15,
                            font: {
                              size: 12,
                              family: "'Open Sans', sans-serif"
                            }
                          }
                        },
                        tooltip: {
                          mode: 'index',
                          intersect: false,
                          backgroundColor: 'rgba(0, 0, 0, 0.8)',
                          padding: 12,
                          titleFont: {
                            size: 13,
                            family: "'Open Sans', sans-serif"
                          },
                          bodyFont: {
                            size: 12,
                            family: "'Open Sans', sans-serif"
                          }
                        }
                      },
                      scales: {
                        x: {
                          grid: {
                            display: false
                          },
                          ticks: {
                            font: {
                              size: 11,
                              family: "'Open Sans', sans-serif"
                            }
                          }
                        },
                        y: {
                          beginAtZero: true,
                          grid: {
                            color: 'rgba(0, 0, 0, 0.05)'
                          },
                          ticks: {
                            font: {
                              size: 11,
                              family: "'Open Sans', sans-serif"
                            }
                          }
                        }
                      },
                      interaction: {
                        mode: 'nearest',
                        axis: 'x',
                        intersect: false
                      }
                    }
                  });
                })
                .catch(error => {
                  console.error('Error loading chart data:', error);
                  ctx.parentElement.innerHTML = '<p style="text-align:center; color:#6B778C; padding:40px;">Failed to load chart data</p>';
                });
            })();
            </script>
            
            <?php } // End PDO check for dashboard ?>

          <?php elseif ($page === 'tracking'): ?>
            <?php
            // Handle tracking settings update
            if (isset($_POST['action']) && $_POST['action'] === 'save_tracking_settings') {
                $openTracking = isset($_POST['open_tracking_enabled']) ? '1' : '0';
                $clickTracking = isset($_POST['click_tracking_enabled']) ? '1' : '0';
                
                set_setting($pdo, 'open_tracking_enabled', $openTracking);
                set_setting($pdo, 'click_tracking_enabled', $clickTracking);
                
                echo '<div style="padding:12px; margin-bottom:16px; background:var(--sg-success-light); color:var(--sg-success); border-radius:6px; border:1px solid var(--sg-success);">✓ Tracking settings saved successfully</div>';
            }
            
            $openTrackingEnabled = get_setting($pdo, 'open_tracking_enabled', '1') === '1';
            $clickTrackingEnabled = get_setting($pdo, 'click_tracking_enabled', '1') === '1';
            ?>
            
            <div class="page-title">Tracking Settings</div>
            <div class="page-subtitle">Global tracking configuration for all campaigns</div>

            <div class="card">
              <div class="card-header">
                <div>
                  <div class="card-title">Email Tracking</div>
                  <div class="card-subtitle">Configure open and click tracking for all campaigns globally</div>
                </div>
              </div>

              <form method="post">
                <input type="hidden" name="action" value="save_tracking_settings">
                
                <div class="form-row">
                  <div class="form-group">
                    <label style="font-weight:600;">Open Tracking</label>
                    <div class="checkbox-row">
                      <input type="checkbox" name="open_tracking_enabled" id="open_tracking_enabled" <?php if ($openTrackingEnabled) echo 'checked'; ?>>
                      <label for="open_tracking_enabled">Enable Open Tracking</label>
                    </div>
                    <small class="hint">Track when recipients open your emails using an invisible tracking pixel. This applies to all campaigns globally.</small>
                  </div>
                </div>

                <div class="form-row">
                  <div class="form-group">
                    <label style="font-weight:600;">Click Tracking</label>
                    <div class="checkbox-row">
                      <input type="checkbox" name="click_tracking_enabled" id="click_tracking_enabled" <?php if ($clickTrackingEnabled) echo 'checked'; ?>>
                      <label for="click_tracking_enabled">Enable Click Tracking</label>
                    </div>
                    <small class="hint">Track when recipients click links in your emails by wrapping URLs with tracking redirects. This applies to all campaigns globally.</small>
                  </div>
                </div>

                <div style="margin-top:20px;">
                  <button type="submit" class="btn btn-primary">Save Tracking Settings</button>
                </div>
              </form>

              <div style="margin-top:32px; padding-top:24px; border-top:1px solid var(--sg-border);">
                <div style="font-weight:600; margin-bottom:12px;">How Tracking Works</div>
                <div style="color:var(--sg-text-muted); line-height:1.6;">
                  <p><strong>Open Tracking:</strong> Adds a 1x1 pixel invisible image to detect when emails are opened.</p>
                  <p><strong>Click Tracking:</strong> Wraps all links in your emails with tracking redirects to record clicks before sending users to the original destination.</p>
                  <p style="margin-bottom:0;"><strong>Note:</strong> These settings apply globally to all campaigns. When disabled, no tracking code will be injected into outgoing emails.</p>
                </div>
              </div>
            </div>

          <?php else: ?>
            <p>Unknown page.</p>
          <?php endif; ?>
        </div>
      </div>
    </div>

    <script>
      // Mobile menu functions
      function toggleMobileMenu() {
        var navMain = document.querySelector('.nav-main');
        var navOverlay = document.getElementById('navOverlay');
        
        if (navMain.classList.contains('mobile-open')) {
          closeMobileMenu();
        } else {
          navMain.classList.add('mobile-open');
          navOverlay.classList.add('active');
          document.body.style.overflow = 'hidden';
        }
      }
      
      function closeMobileMenu() {
        var navMain = document.querySelector('.nav-main');
        var navOverlay = document.getElementById('navOverlay');
        
        navMain.classList.remove('mobile-open');
        navOverlay.classList.remove('active');
        document.body.style.overflow = '';
      }
      
      // Close mobile menu when clicking nav links
      document.querySelectorAll('.nav-link').forEach(function(link) {
        link.addEventListener('click', function() {
          if (window.innerWidth <= 900) {
            closeMobileMenu();
          }
        });
      });

      (function(){
        var spOpenBtn = document.getElementById('spOpenBtn');
        var spCloseBtn = document.getElementById('spCloseBtn');
        var spSidebar = document.getElementById('spSidebar');
        var pageWrapper = document.getElementById('pageWrapper');

        function openSidebar() {
          spSidebar.classList.add('open');
          pageWrapper.classList.add('shifted');
        }
        function closeSidebar() {
          spSidebar.classList.remove('open');
          pageWrapper.classList.remove('shifted');
        }

        if (spOpenBtn) spOpenBtn.addEventListener('click', openSidebar);
        if (spCloseBtn) spCloseBtn.addEventListener('click', closeSidebar);

        // Toggle profile fields when switching type
        var pfType = document.getElementById('pf_type');
        if (pfType) {
          pfType.addEventListener('change', function() {
            var v = pfType.value;
            var smtpFields = document.getElementById('pf_smtp_fields');
            var apiFields = document.getElementById('pf_api_fields');
            if (v === 'api') {
              smtpFields.style.display = 'none';
              apiFields.style.display = '';
            } else {
              smtpFields.style.display = '';
              apiFields.style.display = 'none';
            }
          });
        }

        // Open sidebar if editing profile
        <?php if ($editProfile): ?>
          openSidebar();
        <?php endif; ?>

        // Toast helper exposed to global scope
        window.showToast = function(msg, type){
          var t = document.getElementById('globalToast');
          if(!t) return;
          t.innerText = msg;
          t.className = 'toast show ' + (type === 'error' ? 'error' : 'success');
          setTimeout(function(){
            t.className = 'toast';
          }, 2000);
        };

        // Show notifications for query params (e.g., test_sent, sent)
        (function(){
          var params = new URLSearchParams(window.location.search);
          if (params.has('test_sent')) {
            showToast('Test message(s) sent', 'success');
          }
          if (params.has('sent')) {
            showToast('Campaign queued for sending', 'success');
          }
          if (params.has('save_error')) {
            showToast('Save error', 'error');
          }
          if (params.has('no_recipients')) {
            showToast('No recipients to send to (all invalid or unsubscribed)', 'error');
          }
          if (params.has('cleared_unsubscribes')) {
            showToast('All unsubscribes cleared', 'success');
          }
          if (params.has('cleared_bounces')) {
            showToast('All bounces cleared', 'success');
          }
        })();

        // Wire up Check Connection buttons (AJAX)
        document.querySelectorAll('.check-conn-btn').forEach(function(btn){
          btn.addEventListener('click', function(){
            var pid = btn.getAttribute('data-pid');
            var statusEl = document.getElementById('profile-conn-status-' + pid);
            if (!statusEl) return;
            statusEl.innerText = 'Checking...';
            var fd = new FormData();
            fd.append('action', 'check_connection_profile');
            fd.append('profile_id', pid);

            fetch(window.location.pathname + window.location.search, {
              method: 'POST',
              body: fd,
              credentials: 'same-origin'
            }).then(function(r){ return r.json(); })
              .then(function(json){
                if (json && json.ok) {
                  statusEl.style.color = 'green';
                  statusEl.innerText = json.msg || 'OK';
                  showToast('Connection OK', 'success');
                } else {
                  statusEl.style.color = 'red';
                  statusEl.innerText = (json && json.msg) ? json.msg : 'Connection failed';
                  showToast('Connection failed', 'error');
                }
              }).catch(function(err){
                statusEl.style.color = 'red';
                statusEl.innerText = 'Connection error';
                showToast('Connection error', 'error');
              });
          });
        });

        // Wire up Test Proxy buttons (AJAX)
        document.querySelectorAll('.check-proxy-btn').forEach(function(btn){
          btn.addEventListener('click', function(){
            var pid = btn.getAttribute('data-pid');
            var statusEl = document.getElementById('profile-conn-status-' + pid);
            if (!statusEl) return;
            statusEl.innerText = 'Testing proxy...';
            statusEl.style.color = 'var(--sg-muted)';
            var fd = new FormData();
            fd.append('action', 'check_proxy');
            fd.append('profile_id', pid);

            fetch(window.location.pathname + window.location.search, {
              method: 'POST',
              body: fd,
              credentials: 'same-origin'
            }).then(function(r){ return r.json(); })
              .then(function(json){
                if (json && json.ok) {
                  statusEl.style.color = 'green';
                  statusEl.innerText = json.msg || 'Proxy OK';
                  showToast('Proxy OK: ' + (json.ip || ''), 'success');
                } else {
                  statusEl.style.color = 'red';
                  statusEl.innerText = (json && json.msg) ? json.msg : 'Proxy test failed';
                  showToast('Proxy test failed', 'error');
                }
              }).catch(function(err){
                statusEl.style.color = 'red';
                statusEl.innerText = 'Proxy test error';
                showToast('Proxy test error', 'error');
              });
          });
        });

        // Toggle proxy fields when checkbox changes
        var proxyChk = document.getElementById('pf_proxy_enabled');
        var proxyFields = document.getElementById('pf_proxy_fields');
        if (proxyChk && proxyFields) {
          proxyChk.addEventListener('change', function(){
            proxyFields.style.display = proxyChk.checked ? '' : 'none';
          });
        }

        // Inline proxy live-test button (uses current form values, no save needed)
        var proxyTestBtn = document.getElementById('pf_proxy_test_btn');
        var proxyTestResult = document.getElementById('pf_proxy_test_result');
        if (proxyTestBtn && proxyTestResult) {
          proxyTestBtn.addEventListener('click', function(){
            var form = document.getElementById('profileForm');
            if (!form) return;
            proxyTestResult.style.color = 'var(--sg-muted)';
            proxyTestResult.innerText = 'Testing…';

            var elType = form.querySelector('[name="proxy_type"]');
            var elHost = form.querySelector('[name="proxy_host"]');
            var elPort = form.querySelector('[name="proxy_port"]');
            var elUser = form.querySelector('[name="proxy_user"]');
            var elPass = form.querySelector('[name="proxy_pass"]');

            var fd = new FormData();
            fd.append('action', 'test_proxy_live');
            fd.append('proxy_type', elType ? elType.value : 'http');
            fd.append('proxy_host', elHost ? elHost.value : '');
            fd.append('proxy_port', elPort ? elPort.value : '8080');
            fd.append('proxy_user', elUser ? elUser.value : '');
            fd.append('proxy_pass', elPass ? elPass.value : '');

            fetch(window.location.pathname + window.location.search, {
              method: 'POST',
              body: fd,
              credentials: 'same-origin'
            }).then(function(r){ return r.json(); })
              .then(function(json){
                if (json && json.ok) {
                  proxyTestResult.style.color = 'green';
                  proxyTestResult.innerText = '✔ ' + (json.msg || 'OK');
                } else {
                  proxyTestResult.style.color = 'red';
                  proxyTestResult.innerText = '✘ ' + ((json && json.msg) ? json.msg : 'Test failed');
                }
              }).catch(function(){
                proxyTestResult.style.color = 'red';
                proxyTestResult.innerText = '✘ Request error';
              });
          });
        }

      })();
    </script>

    </body>
    </html>