<?php
/**
 * Petition Form Plugin
 *
 * GDPR-compliant petition system with double opt-in email confirmation.
 * File-based storage (CSV) with proper locking to prevent corruption.
 *
 * Usage: Add to metadata.ini:
 *   plugins = "petition-form"
 *   petition_id = "my-petition"
 *   thank_you_page = "takk" (optional, defaults to "takk")
 */

// Start session if not already started
if (session_status() === PHP_SESSION_NONE) {
    session_start();
}

// Generate CSRF token if not exists
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// --- Helper Functions ---

/**
 * Sanitize value to prevent CSV injection attacks
 * Prefixes values starting with dangerous characters that could be interpreted as formulas
 */
function petitionSanitizeCSV(string $value): string {
    if (empty($value)) {
        return $value;
    }

    $firstChar = substr($value, 0, 1);
    $dangerousChars = ['=', '+', '-', '@', "\t", "\r", "\n"];

    if (in_array($firstChar, $dangerousChars, true)) {
        return "'" . $value;
    }

    return $value;
}

/**
 * Subscribe email to Listmonk newsletter lists via public API
 * Listmonk handles double opt-in (sends its own confirmation email)
 */
function petitionSubscribeToNewsletter(string $email, string $name, array $listUuids = []): bool {
    $configPath = dirname(__DIR__, 2) . '/listmonk-config.php';
    if (!file_exists($configPath)) {
        error_log("Listmonk config not found: {$configPath}");
        return false;
    }

    $config = require $configPath;
    if (!$config['enabled']) {
        error_log("Listmonk disabled in config");
        return false;
    }

    $uuids = !empty($listUuids) ? $listUuids : $config['list_uuids'];

    // Log the UUIDs being used
    error_log("Listmonk attempting subscription with UUIDs: " . implode(', ', $uuids));

    $payload = json_encode([
        'email' => $email,
        'name' => $name,
        'list_uuids' => $uuids
    ]);

    $url = $config['url'] . '/api/public/subscription';
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $payload,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10
    ]);

    $response = curl_exec($ch);
    $curlError = curl_error($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($curlError) {
        error_log("Listmonk curl error: {$curlError}");
        return false;
    }

    if ($httpCode < 200 || $httpCode >= 300) {
        error_log("Listmonk subscription failed: HTTP {$httpCode}, URL: {$url}, Response: {$response}");
        return false;
    }

    error_log("Listmonk subscription success for {$email}");
    return true;
}

/**
 * Check IP-based rate limiting to prevent email bombing and spam
 * Returns true if IP is allowed, false if rate limit exceeded
 */
function petitionCheckIPRateLimit(string $petitionId, int $maxAttempts = 3, int $windowSeconds = 300): bool {
    $ipAddress = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
    $ipHash = hash('sha256', $ipAddress . $petitionId);

    $rateLimitFile = dirname(__DIR__, 2) . '/data/petition-rate-limit.csv';
    $dir = dirname($rateLimitFile);

    if (!is_dir($dir)) {
        mkdir($dir, 0750, true);
    }

    $currentTime = time();
    $cutoffTime = $currentTime - $windowSeconds;

    // Create file if it doesn't exist
    if (!file_exists($rateLimitFile)) {
        touch($rateLimitFile);
        chmod($rateLimitFile, 0644);
    }

    $fp = fopen($rateLimitFile, 'a+');
    if (!$fp) {
        // If we can't open file, allow request (fail open for availability)
        return true;
    }

    if (!flock($fp, LOCK_EX)) {
        fclose($fp);
        return true;
    }

    // Read existing attempts
    rewind($fp);
    $attempts = [];
    $otherEntries = [];

    while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
        if (!isset($row[0]) || !isset($row[1])) {
            continue;
        }

        $timestamp = (int)$row[0];
        $storedIpHash = $row[1];

        // Clean up old entries (older than window)
        if ($timestamp < $cutoffTime) {
            continue;
        }

        // Separate our IP's attempts from others
        if ($storedIpHash === $ipHash) {
            $attempts[] = $timestamp;
        } else {
            $otherEntries[] = $row;
        }
    }

    // Check if rate limit exceeded
    $recentAttempts = count($attempts);
    if ($recentAttempts >= $maxAttempts) {
        flock($fp, LOCK_UN);
        fclose($fp);
        return false;
    }

    // Add current attempt
    $attempts[] = $currentTime;

    // Rewrite file with cleaned data
    rewind($fp);
    ftruncate($fp, 0);

    // Write back other IPs' recent attempts
    foreach ($otherEntries as $entry) {
        fputcsv($fp, $entry, ',', '"', '');
    }

    // Write our attempts
    foreach ($attempts as $timestamp) {
        fputcsv($fp, [$timestamp, $ipHash], ',', '"', '');
    }

    flock($fp, LOCK_UN);
    fclose($fp);

    return true;
}

/**
 * Get translation from language files with section support
 */
function petitionT(Context $ctx, string $section, string $key, array $replacements = []): string {
    $translations = $ctx->get('translations', []);
    $fullKey = "{$section}.{$key}";

    $text = $translations[$fullKey] ?? $key;

    foreach ($replacements as $placeholder => $value) {
        $text = str_replace("{{$placeholder}}", $value, $text);
    }

    return $text;
}

/**
 * Get regions from language file
 */
function petitionGetRegions(Context $ctx): array {
    $translations = $ctx->get('translations', []);
    $regions = [];

    $regionKeys = [
        'agder', 'akershus', 'buskerud', 'finnmark', 'innlandet',
        'more_og_romsdal', 'nordland', 'oslo', 'rogaland', 'telemark',
        'troms', 'trondelag', 'vestfold', 'vestland', 'ostfold'
    ];

    foreach ($regionKeys as $key) {
        $fullKey = "regions.{$key}";
        $regions[$key] = $translations[$fullKey] ?? $key;
    }

    return $regions;
}

/**
 * Get path to petition CSV file
 */
function petitionGetCsvPath(string $petitionId): string {
    // Sanitize petition ID
    $petitionId = preg_replace('/[^a-z0-9\-_]/i', '', $petitionId);

    // Additional path traversal protection
    if (empty($petitionId) || strpos($petitionId, '..') !== false || strpos($petitionId, '/') !== false || strpos($petitionId, '\\') !== false) {
        throw new Exception('Invalid petition ID');
    }

    return dirname(__DIR__, 2) . "/data/petitions/{$petitionId}.csv";
}

/**
 * Extract petition ID from the resolved page directory path
 * Uses the last directory segment (the petition's slug) as the ID
 */
function petitionGetIdFromPath(string $pageDir): ?string {
    // Get the last directory segment as the petition ID
    $petitionSlug = basename($pageDir);

    // Sanitize: remove any leading numeric prefix (e.g., "01-" becomes just the slug part)
    // This handles folders like "01-my-petition" -> "my-petition"
    $petitionSlug = preg_replace('/^\d+-/', '', $petitionSlug);

    if (empty($petitionSlug)) {
        return null;
    }

    return $petitionSlug;
}

/**
 * Append a new signature to the CSV file with file locking
 * Now includes duplicate email check inside the lock to prevent race conditions
 */
function petitionAppendSignature(string $csvPath, array $data): bool {
    $dir = dirname($csvPath);
    if (!is_dir($dir)) {
        mkdir($dir, 0750, true);
    }

    $isNewFile = !file_exists($csvPath);

    // Open for reading and appending
    $fp = fopen($csvPath, 'a+');
    if (!$fp) {
        return false;
    }

    if (flock($fp, LOCK_EX)) {
        // Check for duplicate email INSIDE the lock to prevent race conditions
        rewind($fp);
        $foundDuplicate = false;

        if (!$isNewFile) {
            fgetcsv($fp, null, ',', '"', ''); // Skip header

            while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
                if (isset($row[1]) && strtolower($row[1]) === strtolower($data['email'])) {
                    $foundDuplicate = true;
                    break;
                }
            }
        }

        if ($foundDuplicate) {
            flock($fp, LOCK_UN);
            fclose($fp);
            return false;
        }

        // Write header if new file
        if ($isNewFile) {
            fputcsv($fp, ['timestamp', 'email', 'firstname', 'surname', 'region', 'display', 'status', 'token', 'token_created', 'ip_hash'], ',', '"', '');
        }

        // Sanitize user input to prevent CSV injection
        fputcsv($fp, [
            $data['timestamp'],
            petitionSanitizeCSV($data['email']),
            petitionSanitizeCSV($data['firstname']),
            petitionSanitizeCSV($data['surname']),
            $data['region'],
            $data['display'],
            $data['status'],
            $data['token'],
            $data['token_created'],
            $data['ip_hash']
        ], ',', '"', '');

        flock($fp, LOCK_UN);
        fclose($fp);
        return true;
    }

    fclose($fp);
    return false;
}

/**
 * Update signature status (for confirmation)
 * Includes 30-day token expiry check
 */
function petitionConfirmSignature(string $csvPath, string $token): string {
    if (!file_exists($csvPath)) {
        return 'error';
    }

    $fp = fopen($csvPath, 'r+');
    if (!$fp) {
        return 'error';
    }

    if (!flock($fp, LOCK_EX)) {
        fclose($fp);
        return 'error';
    }

    $rows = [];
    $header = fgetcsv($fp, null, ',', '"', '');
    $rows[] = $header;

    $found = false;
    $alreadyConfirmed = false;
    $tokenExpired = false;
    $currentTime = time();

    // Token is at index 7, token_created is at index 8
    while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
        if (isset($row[7]) && $row[7] === $token) {
            $found = true;

            // Check if token is expired (30 days = 2592000 seconds)
            $tokenCreated = isset($row[8]) ? (int)$row[8] : 0;
            if ($tokenCreated > 0 && ($currentTime - $tokenCreated) > 2592000) {
                $tokenExpired = true;
            } elseif ($row[6] === 'confirmed') {
                $alreadyConfirmed = true;
            } else {
                $row[6] = 'confirmed';
            }
        }
        $rows[] = $row;
    }

    if (!$found) {
        flock($fp, LOCK_UN);
        fclose($fp);
        return 'error';
    }

    if ($tokenExpired) {
        flock($fp, LOCK_UN);
        fclose($fp);
        return 'expired';
    }

    if ($alreadyConfirmed) {
        flock($fp, LOCK_UN);
        fclose($fp);
        return 'already';
    }

    // Rewrite file
    rewind($fp);
    ftruncate($fp, 0);

    foreach ($rows as $row) {
        fputcsv($fp, $row, ',', '"', '');
    }

    flock($fp, LOCK_UN);
    fclose($fp);

    return 'success';
}

/**
 * Delete a signature by token (for GDPR compliance)
 */
function petitionDeleteSignature(string $csvPath, string $token): string {
    if (!file_exists($csvPath)) {
        return 'error';
    }

    $fp = fopen($csvPath, 'r+');
    if (!$fp) {
        return 'error';
    }

    if (!flock($fp, LOCK_EX)) {
        fclose($fp);
        return 'error';
    }

    $rows = [];
    $header = fgetcsv($fp, null, ',', '"', '');
    $rows[] = $header;

    $found = false;

    // Token is at index 7
    while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
        if (isset($row[7]) && $row[7] === $token) {
            $found = true;
            // Skip this row (don't add to $rows) to delete it
            continue;
        }
        $rows[] = $row;
    }

    if (!$found) {
        flock($fp, LOCK_UN);
        fclose($fp);
        return 'error';
    }

    // Rewrite file without the deleted row
    rewind($fp);
    ftruncate($fp, 0);

    foreach ($rows as $row) {
        fputcsv($fp, $row, ',', '"', '');
    }

    flock($fp, LOCK_UN);
    fclose($fp);

    return 'success';
}

/**
 * Check if email already exists in petition
 */
function petitionEmailExists(string $csvPath, string $email): bool {
    if (!file_exists($csvPath)) {
        return false;
    }

    $fp = fopen($csvPath, 'r');
    if (!$fp) {
        return false;
    }

    if (flock($fp, LOCK_SH)) {
        fgetcsv($fp, null, ',', '"', ''); // Skip header

        while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
            if (isset($row[1]) && strtolower($row[1]) === strtolower($email)) {
                flock($fp, LOCK_UN);
                fclose($fp);
                return true;
            }
        }

        flock($fp, LOCK_UN);
    }

    fclose($fp);
    return false;
}

/**
 * Get all confirmed signatures
 */
function petitionGetConfirmedSignatures(string $csvPath): array {
    if (!file_exists($csvPath)) {
        return [];
    }

    $signatures = [];
    $fp = fopen($csvPath, 'r');

    if (!$fp) {
        return [];
    }

    if (flock($fp, LOCK_SH)) {
        fgetcsv($fp, null, ',', '"', ''); // Skip header

        // CSV format: timestamp, email, firstname, surname, region, display, status, token, ip_hash
        while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
            if (isset($row[6]) && $row[6] === 'confirmed') {
                $signatures[] = [
                    'timestamp' => (int)$row[0],
                    'firstname' => $row[2],
                    'surname' => $row[3],
                    'region' => $row[4],
                    'display' => $row[5]
                ];
            }
        }

        flock($fp, LOCK_UN);
    }

    fclose($fp);

    // Sort newest first
    usort($signatures, fn($a, $b) => $b['timestamp'] - $a['timestamp']);

    return $signatures;
}

/**
 * Format signature for display based on privacy preference
 */
function petitionFormatSignature(array $signature, Context $ctx): string {
    $regions = petitionGetRegions($ctx);
    $regionDisplay = $regions[$signature['region']] ?? $signature['region'];

    $fromLabel = petitionT($ctx, 'petition', 'from_region');

    switch ($signature['display']) {
        case 'anonymous':
            $anonLabel = petitionT($ctx, 'petition', 'anonymous_name');
            return $anonLabel . ' ' . $fromLabel . ' ' . $regionDisplay;

        case 'full':
            $fullName = htmlspecialchars($signature['firstname'] . ' ' . $signature['surname']);
            return $fullName . ' ' . $fromLabel . ' ' . $regionDisplay;

        case 'semi':
        default:
            return htmlspecialchars($signature['firstname']) . ' ' . $fromLabel . ' ' . $regionDisplay;
    }
}

/**
 * Format timestamp for display
 */
function petitionFormatDate(int $timestamp, Context $ctx): string {
    $currentLang = $ctx->get('currentLang', 'no');

    $months = [
        'no' => ['januar', 'februar', 'mars', 'april', 'mai', 'juni',
                 'juli', 'august', 'september', 'oktober', 'november', 'desember'],
        'en' => ['January', 'February', 'March', 'April', 'May', 'June',
                 'July', 'August', 'September', 'October', 'November', 'December']
    ];

    $monthNames = $months[$currentLang] ?? $months['no'];

    $day = date('j', $timestamp);
    $month = $monthNames[(int)date('n', $timestamp) - 1];
    $year = date('Y', $timestamp);
    $time = date('H:i', $timestamp);

    if ($currentLang === 'en') {
        return "{$month} {$day}, {$year}, {$time}";
    }

    return "{$day}. {$month} {$year}, {$time}";
}

/**
 * Log email send attempt to SMTP log file
 * Used for tracking failed sends and enabling retry via CLI
 */
function petitionLogEmail(string $type, string $petitionId, string $email, bool $success, string $errorMessage = ''): void {
    $logPath = dirname(__DIR__, 2) . '/data/smtp-log.csv';
    $dir = dirname($logPath);

    if (!is_dir($dir)) {
        mkdir($dir, 0750, true);
    }

    $isNewFile = !file_exists($logPath);

    $fp = fopen($logPath, 'a');
    if (!$fp) {
        return;
    }

    if (flock($fp, LOCK_EX)) {
        if ($isNewFile) {
            fputcsv($fp, ['timestamp', 'type', 'petition_id', 'email', 'status', 'error_message'], ',', '"', '');
        }

        fputcsv($fp, [
            time(),
            $type,
            $petitionId,
            $email,
            $success ? 'success' : 'failed',
            $errorMessage
        ], ',', '"', '');

        flock($fp, LOCK_UN);
    }

    fclose($fp);
}

/**
 * Retry wrapper for email sending with exponential backoff
 * Retries up to maxRetries times with increasing delays
 */
function petitionSendWithRetry(callable $sendFunction, int $maxRetries = 3): bool {
    $delays = [2, 4, 8]; // Exponential backoff: 2s, 4s, 8s

    for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
        $result = $sendFunction();

        if ($result === true) {
            if ($attempt > 1) {
                // Log successful retry (no sensitive data)
                error_log("Petition email succeeded on attempt {$attempt}/{$maxRetries}");
            }
            return true;
        }

        // If not the last attempt, wait before retrying
        if ($attempt < $maxRetries) {
            $delay = $delays[$attempt - 1] ?? 8;
            sleep($delay);
        }
    }

    // All retries failed
    error_log("Petition email failed after {$maxRetries} attempts");
    return false;
}

/**
 * Send confirmation email (with retry wrapper)
 */
function petitionSendConfirmationEmail(array $data, string $confirmUrl, string $petitionTitle, string $petitionId, Context $ctx): bool {
    $result = petitionSendWithRetry(function() use ($data, $confirmUrl, $petitionTitle, $ctx, &$errorMessage) {
        return petitionSendConfirmationEmailInternal($data, $confirmUrl, $petitionTitle, $ctx, $errorMessage);
    });

    // Log the final result
    petitionLogEmail('confirmation', $petitionId, $data['email'], $result, $result ? '' : ($errorMessage ?? 'Unknown error'));

    return $result;
}

/**
 * Internal: Send confirmation email (single attempt)
 */
function petitionSendConfirmationEmailInternal(array $data, string $confirmUrl, string $petitionTitle, Context $ctx, ?string &$errorMessage = null): bool {
    // Decode HTML entities for plain-text email (e.g., &ndash; → –)
    $petitionTitle = html_entity_decode($petitionTitle, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $smtpConfigPath = dirname(__DIR__, 2) . '/smtp-config.php';
    if (!file_exists($smtpConfigPath)) {
        $errorMessage = 'SMTP config not found';
        return false;
    }

    $config = require $smtpConfigPath;

    if (!$config['enabled']) {
        $errorMessage = 'SMTP disabled';
        return false;
    }

    // Get petition-specific overrides
    $fromEmail = $config['petition']['from_email'] ?? $config['from_email'];
    $fromName = $config['petition']['from_name'] ?? $config['from_name'];

    // Build email body
    $greeting = petitionT($ctx, 'petition', 'email_greeting', ['name' => $data['firstname']]);
    $thanks = petitionT($ctx, 'petition', 'email_thanks', ['title' => $petitionTitle]);
    $confirmText = petitionT($ctx, 'petition', 'email_confirm');
    $expiryNotice = petitionT($ctx, 'petition', 'email_expiry_notice');
    $ignoreText = petitionT($ctx, 'petition', 'email_ignore');
    $rightsInfo = petitionT($ctx, 'petition', 'email_rights_info');
    $signature = petitionT($ctx, 'petition', 'email_signature');
    $org = petitionT($ctx, 'petition', 'email_org');

    $emailBody = "{$greeting}\n\n";
    $emailBody .= "{$thanks}\n\n";
    $emailBody .= "{$confirmText}\n";
    $emailBody .= "{$confirmUrl}\n\n";
    $emailBody .= "{$expiryNotice}\n\n";
    $emailBody .= "{$ignoreText}\n\n";
    $emailBody .= "---\n\n";
    $emailBody .= "{$rightsInfo}\n\n";
    $emailBody .= "{$signature},\n";
    $emailBody .= "{$org}\n";

    $subject = petitionT($ctx, 'petition', 'email_subject');

    // Get petition-specific SMTP settings (allows separate SMTP account for better deliverability)
    $smtpHost = $config['petition']['host'] ?? $config['host'];
    $smtpPort = $config['petition']['port'] ?? $config['port'];
    $smtpUser = $config['petition']['username'] ?? $config['username'];
    $smtpPass = $config['petition']['password'] ?? $config['password'];

    // Pre-flight check
    $fp = @fsockopen($smtpHost, $smtpPort, $errno, $errstr, 10);
    if (!$fp) {
        $errorMessage = "Connection failed: {$errno} - {$errstr}";
        error_log("Petition SMTP pre-flight failed: {$smtpHost}:{$smtpPort} - {$errno} - {$errstr}");
        return false;
    }
    fclose($fp);

    try {
        require_once dirname(__DIR__, 2) . '/vendor/PHPMailer.Lite.php';

        $mail = new \codeworxtech\PHPMailerLite\PHPMailerLite();

        $mail->SetSMTPhost($smtpHost);
        $mail->SetSMTPport($smtpPort);
        $mail->SetSMTPuser($smtpUser);
        $mail->SetSMTPpass($smtpPass);

        $recipientName = $data['firstname'] . ' ' . $data['surname'];
        $mail->SetSender([$fromEmail => $fromName]);
        $mail->AddRecipient([$data['email'] => $recipientName]);
        $mail->SetSubject($subject);
        $mail->SetBodyText($emailBody);

        ob_start();
        $sent = @$mail->Send('smtp');
        $output = ob_get_clean();

        if (!$sent || stripos($output, 'error') !== false || stripos($output, '&#10007;') !== false) {
            $errorMessage = strip_tags($output) ?: 'Send returned false';
            error_log("Petition email send failed: " . strip_tags($output));
            return false;
        }

        return true;
    } catch (\Exception $e) {
        $errorMessage = $e->getMessage();
        error_log("Petition email exception: " . $e->getMessage());
        return false;
    }
}

/**
 * Get signature data by token
 */
function petitionGetSignatureByToken(string $csvPath, string $token): ?array {
    if (!file_exists($csvPath)) {
        return null;
    }

    $fp = fopen($csvPath, 'r');
    if (!$fp) {
        return null;
    }

    $signature = null;

    if (flock($fp, LOCK_SH)) {
        fgetcsv($fp, null, ',', '"', ''); // Skip header

        // CSV format: timestamp, email, firstname, surname, region, display, status, token, ip_hash
        while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
            if (isset($row[7]) && $row[7] === $token) {
                $signature = [
                    'email' => $row[1],
                    'firstname' => $row[2],
                    'surname' => $row[3]
                ];
                break;
            }
        }

        flock($fp, LOCK_UN);
    }

    fclose($fp);
    return $signature;
}

/**
 * Get pending signature data by email address
 * Returns signature data if found and pending, null otherwise
 */
function petitionGetPendingSignatureByEmail(string $csvPath, string $email): ?array {
    if (!file_exists($csvPath)) {
        return null;
    }

    $fp = fopen($csvPath, 'r');
    if (!$fp) {
        return null;
    }

    $signature = null;
    $email = strtolower(trim($email));

    if (flock($fp, LOCK_SH)) {
        fgetcsv($fp, null, ',', '"', ''); // Skip header

        // CSV format: timestamp, email, firstname, surname, region, display, status, token, token_created, ip_hash
        while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
            if (isset($row[1]) && strtolower($row[1]) === $email) {
                $signature = [
                    'email' => $row[1],
                    'firstname' => $row[2],
                    'surname' => $row[3],
                    'region' => $row[4],
                    'display' => $row[5],
                    'status' => $row[6],
                    'token' => $row[7],
                    'token_created' => $row[8] ?? 0
                ];
                break;
            }
        }

        flock($fp, LOCK_UN);
    }

    fclose($fp);
    return $signature;
}

/**
 * Update token for an existing signature (for resend functionality)
 */
function petitionUpdateSignatureToken(string $csvPath, string $email, string $newToken): bool {
    if (!file_exists($csvPath)) {
        return false;
    }

    $fp = fopen($csvPath, 'r+');
    if (!$fp) {
        return false;
    }

    if (!flock($fp, LOCK_EX)) {
        fclose($fp);
        return false;
    }

    $rows = [];
    $header = fgetcsv($fp, null, ',', '"', '');
    $rows[] = $header;

    $found = false;
    $email = strtolower(trim($email));
    $currentTime = time();

    while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
        if (isset($row[1]) && strtolower($row[1]) === $email && $row[6] === 'pending') {
            $row[7] = $newToken;           // Update token
            $row[8] = $currentTime;        // Update token_created
            $found = true;
        }
        $rows[] = $row;
    }

    if (!$found) {
        flock($fp, LOCK_UN);
        fclose($fp);
        return false;
    }

    // Rewrite file
    rewind($fp);
    ftruncate($fp, 0);

    foreach ($rows as $row) {
        fputcsv($fp, $row, ',', '"', '');
    }

    flock($fp, LOCK_UN);
    fclose($fp);

    return true;
}

/**
 * Send thank you email with delete link (with retry wrapper)
 */
function petitionSendThankYouEmail(string $token, string $deleteUrl, string $petitionTitle, string $petitionId, string $csvPath, Context $ctx): bool {
    $signature = petitionGetSignatureByToken($csvPath, $token);
    $email = $signature['email'] ?? 'unknown';

    $result = petitionSendWithRetry(function() use ($token, $deleteUrl, $petitionTitle, $csvPath, $ctx, &$errorMessage) {
        return petitionSendThankYouEmailInternal($token, $deleteUrl, $petitionTitle, $csvPath, $ctx, $errorMessage);
    });

    // Log the final result
    petitionLogEmail('thankyou', $petitionId, $email, $result, $result ? '' : ($errorMessage ?? 'Unknown error'));

    return $result;
}

/**
 * Internal: Send thank you email with delete link (single attempt)
 */
function petitionSendThankYouEmailInternal(string $token, string $deleteUrl, string $petitionTitle, string $csvPath, Context $ctx, ?string &$errorMessage = null): bool {
    // Decode HTML entities for plain-text email (e.g., &ndash; → –)
    $petitionTitle = html_entity_decode($petitionTitle, ENT_QUOTES | ENT_HTML5, 'UTF-8');

    $signature = petitionGetSignatureByToken($csvPath, $token);
    if (!$signature) {
        $errorMessage = 'Signature not found';
        return false;
    }

    $smtpConfigPath = dirname(__DIR__, 2) . '/smtp-config.php';
    if (!file_exists($smtpConfigPath)) {
        $errorMessage = 'SMTP config not found';
        return false;
    }

    $config = require $smtpConfigPath;

    if (!$config['enabled']) {
        $errorMessage = 'SMTP disabled';
        return false;
    }

    // Get petition-specific overrides
    $fromEmail = $config['petition']['from_email'] ?? $config['from_email'];
    $fromName = $config['petition']['from_name'] ?? $config['from_name'];

    // Build email body
    $greeting = petitionT($ctx, 'petition', 'email_greeting', ['name' => $signature['firstname']]);
    $thankYouText = petitionT($ctx, 'petition', 'email_thankyou_confirmed', ['title' => $petitionTitle]);
    $deleteText = petitionT($ctx, 'petition', 'email_delete_info');
    $rightsInfo = petitionT($ctx, 'petition', 'email_rights_info');
    $emailSignature = petitionT($ctx, 'petition', 'email_signature');
    $org = petitionT($ctx, 'petition', 'email_org');

    $emailBody = "{$greeting}\n\n";
    $emailBody .= "{$thankYouText}\n\n";
    $emailBody .= "{$deleteText}\n";
    $emailBody .= "{$deleteUrl}\n\n";
    $emailBody .= "---\n\n";
    $emailBody .= "{$rightsInfo}\n\n";
    $emailBody .= "{$emailSignature},\n";
    $emailBody .= "{$org}\n";

    $subject = petitionT($ctx, 'petition', 'email_thankyou_subject');

    // Get petition-specific SMTP settings (allows separate SMTP account for better deliverability)
    $smtpHost = $config['petition']['host'] ?? $config['host'];
    $smtpPort = $config['petition']['port'] ?? $config['port'];
    $smtpUser = $config['petition']['username'] ?? $config['username'];
    $smtpPass = $config['petition']['password'] ?? $config['password'];

    // Pre-flight check
    $fp = @fsockopen($smtpHost, $smtpPort, $errno, $errstr, 10);
    if (!$fp) {
        $errorMessage = "Connection failed: {$errno} - {$errstr}";
        error_log("Petition SMTP pre-flight failed: {$smtpHost}:{$smtpPort} - {$errno} - {$errstr}");
        return false;
    }
    fclose($fp);

    try {
        require_once dirname(__DIR__, 2) . '/vendor/PHPMailer.Lite.php';

        $mail = new \codeworxtech\PHPMailerLite\PHPMailerLite();

        $mail->SetSMTPhost($smtpHost);
        $mail->SetSMTPport($smtpPort);
        $mail->SetSMTPuser($smtpUser);
        $mail->SetSMTPpass($smtpPass);

        $recipientName = $signature['firstname'] . ' ' . $signature['surname'];
        $mail->SetSender([$fromEmail => $fromName]);
        $mail->AddRecipient([$signature['email'] => $recipientName]);
        $mail->SetSubject($subject);
        $mail->SetBodyText($emailBody);

        ob_start();
        $sent = @$mail->Send('smtp');
        $output = ob_get_clean();

        if (!$sent || stripos($output, 'error') !== false || stripos($output, '&#10007;') !== false) {
            $errorMessage = strip_tags($output) ?: 'Send returned false';
            error_log("Petition thank you email send failed: " . strip_tags($output));
            return false;
        }

        return true;
    } catch (\Exception $e) {
        $errorMessage = $e->getMessage();
        error_log("Petition thank you email exception: " . $e->getMessage());
        return false;
    }
}

/**
 * Render the petition form HTML
 */
function petitionRenderForm(Context $ctx, array $formData, array $errors, bool $showForm): string {
    $csrfToken = $_SESSION['csrf_token'];
    $currentTime = time();
    $regions = petitionGetRegions($ctx);

    $html = '<section class="petition-form">';
    // $html .= '<h2>' . htmlspecialchars(petitionT($ctx, 'petition', 'form_title')) . '</h2>';

    // Error messages
    if (!empty($errors)) {
        $html .= '<div class="form-errors" role="alert">';
        $html .= '<ul>';
        foreach ($errors as $error) {
            $html .= '<li>' . htmlspecialchars($error) . '</li>';
        }
        $html .= '</ul>';
        $html .= '</div>';
    }

    if ($showForm) {
        $html .= '<form method="post" action="' . htmlspecialchars($_SERVER['REQUEST_URI']) . '" class="petition-form-inner">';

        // Honeypot
        $html .= '<div class="hp-field" aria-hidden="true" style="position:absolute;left:-9999px">';
        $html .= '<label for="website">Website</label>';
        $html .= '<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">';
        $html .= '</div>';

        // Hidden fields
        $html .= '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($csrfToken) . '">';
        $html .= '<input type="hidden" name="form_start_time" value="' . $currentTime . '">';

        // Name fields (firstname + surname on same row)
        $html .= '<div class="form-row">';

        $html .= '<div class="form-group">';
        $html .= '<label for="petition_firstname">' . htmlspecialchars(petitionT($ctx, 'petition', 'firstname_label')) . ' <span class="required">*</span></label>';
        $html .= '<input type="text" id="petition_firstname" name="firstname" value="' . htmlspecialchars($formData['firstname']) . '" required maxlength="50" placeholder="' . htmlspecialchars(petitionT($ctx, 'petition', 'firstname_placeholder')) . '">';
        $html .= '</div>';

        $html .= '<div class="form-group">';
        $html .= '<label for="petition_surname">' . htmlspecialchars(petitionT($ctx, 'petition', 'surname_label')) . ' <span class="required">*</span></label>';
        $html .= '<input type="text" id="petition_surname" name="surname" value="' . htmlspecialchars($formData['surname']) . '" required maxlength="50" placeholder="' . htmlspecialchars(petitionT($ctx, 'petition', 'surname_placeholder')) . '">';
        $html .= '</div>';

        $html .= '</div>';

        // Email field
        $html .= '<div class="form-group">';
        $html .= '<label for="petition_email">' . htmlspecialchars(petitionT($ctx, 'petition', 'email_label')) . ' <span class="required">*</span></label>';
        $html .= '<input type="email" id="petition_email" name="email" value="' . htmlspecialchars($formData['email']) . '" required maxlength="100" placeholder="' . htmlspecialchars(petitionT($ctx, 'petition', 'email_placeholder')) . '">';
        $html .= '<small>' . htmlspecialchars(petitionT($ctx, 'petition', 'email_help')) . '</small>';
        $html .= '</div>';

        // Region dropdown
        $html .= '<div class="form-group">';
        $html .= '<label for="petition_region">' . htmlspecialchars(petitionT($ctx, 'petition', 'region_label')) . ' <span class="required">*</span></label>';
        $html .= '<select id="petition_region" name="region" required>';
        $html .= '<option value="">' . htmlspecialchars(petitionT($ctx, 'petition', 'region_placeholder')) . '</option>';
        foreach ($regions as $key => $label) {
            $selected = $formData['region'] === $key ? ' selected' : '';
            $html .= '<option value="' . htmlspecialchars($key) . '"' . $selected . '>' . htmlspecialchars($label) . '</option>';
        }
        $html .= '</select>';
        $html .= '</div>';

        // Display preference
        $html .= '<fieldset class="form-group">';
        $html .= '<legend>' . htmlspecialchars(petitionT($ctx, 'petition', 'display_legend')) . '</legend>';

        $semiChecked = ($formData['display'] === 'semi' || empty($formData['display'])) ? ' checked' : '';
        $html .= '<label class="radio-label">';
        $html .= '<input type="radio" name="display" value="semi"' . $semiChecked . '>';
        $html .= ' ' . htmlspecialchars(petitionT($ctx, 'petition', 'display_semi'));
        $html .= '</label>';

        $anonChecked = $formData['display'] === 'anonymous' ? ' checked' : '';
        $html .= '<label class="radio-label">';
        $html .= '<input type="radio" name="display" value="anonymous"' . $anonChecked . '>';
        $html .= ' ' . htmlspecialchars(petitionT($ctx, 'petition', 'display_anonymous'));
        $html .= '</label>';

        $fullChecked = $formData['display'] === 'full' ? ' checked' : '';
        $html .= '<label class="radio-label">';
        $html .= '<input type="radio" name="display" value="full"' . $fullChecked . '>';
        $html .= ' ' . htmlspecialchars(petitionT($ctx, 'petition', 'display_full'));
        $html .= '</label>';

        $html .= '</fieldset>';

        // Privacy summary box
        $langPrefix = $ctx->get('langPrefix', '');
        $privacyUrl = $langPrefix . '/personvern/';
        $html .= '<div class="form-group info-box info-box--yellow">';
        $html .= '<p>' . petitionT($ctx, 'petition', 'privacy_summary') . ' ';
        $html .= '<a href="' . htmlspecialchars($privacyUrl) . '" target="_blank">' . htmlspecialchars(petitionT($ctx, 'petition', 'privacy_policy_link')) . '</a></p>';
        $html .= '</div>';

        // GDPR Consent checkbox + Newsletter subscription
        $consentChecked = isset($formData['gdpr_consent']) && $formData['gdpr_consent'] === 'on' ? ' checked' : '';
        $newsletterChecked = isset($formData['newsletter']) && $formData['newsletter'] === 'on' ? ' checked' : '';
        $html .= '<div class="form-group info-box info-box--green consent-group">';
        $html .= '<label class="consent-label">';
        $html .= '<input type="checkbox" name="gdpr_consent" id="gdpr_consent" required' . $consentChecked . '> ';
        $html .= '<span>' . petitionT($ctx, 'petition', 'gdpr_consent_text') . ' <span class="required">*</span></span>';
        $html .= '</label>';
        $html .= '<label class="consent-label">';
        $html .= '<input type="checkbox" name="newsletter" id="newsletter"' . $newsletterChecked . '> ';
        $html .= '<span>' . htmlspecialchars(petitionT($ctx, 'petition', 'newsletter_subscribe')) . '</span>';
        $html .= '</label>';
        $html .= '</div>';

        // Submit button
        $html .= '<div class="form-group">';
        $html .= '<button type="submit" name="petition_submit" class="button">' . htmlspecialchars(petitionT($ctx, 'petition', 'submit_button')) . '</button>';
        $html .= '</div>';

        $html .= '</form>';
    }

    $html .= '</section>';

    return $html;
}

/**
 * Render the signatures list HTML
 */
function petitionRenderSignatures(array $signatures, Context $ctx): string {
    $count = count($signatures);

    $countText = $count === 1
        ? petitionT($ctx, 'petition', 'signature_count', ['count' => $count])
        : petitionT($ctx, 'petition', 'signature_count_plural', ['count' => $count]);

    $html = '<section class="petition-signatures">';
    $html .= '<h2>' . htmlspecialchars(petitionT($ctx, 'petition', 'signatures_title')) . ' (' . $count . ')</h2>';

    if ($count > 0) {
        $html .= '<p class="signature-order">' . htmlspecialchars(petitionT($ctx, 'petition', 'newest_first')) . '</p>';
        $html .= '<ul class="signature-list">';

        foreach ($signatures as $sig) {
            $html .= '<li>';
            $html .= '<span class="signature-name">' . petitionFormatSignature($sig, $ctx) . '</span>';
            $html .= '<span class="signature-date">' . petitionFormatDate($sig['timestamp'], $ctx) . '</span>';
            $html .= '</li>';
        }

        $html .= '</ul>';
    }

    $html .= '</section>';

    return $html;
}

/**
 * Resolve URL slug path to actual filesystem path
 */
function petitionResolvePageDir(Context $ctx): ?string {
    $requestPath = $ctx->requestPath;

    if (empty($requestPath)) {
        return $ctx->contentDir;
    }

    $pathParts = explode('/', trim($requestPath, '/'));
    $resolvedPath = $ctx->contentDir;

    foreach ($pathParts as $slug) {
        $resolved = resolveSlugToFolder($resolvedPath, $slug);
        if ($resolved === null) {
            return null;
        }
        $resolvedPath .= '/' . $resolved;
    }

    return $resolvedPath;
}

/**
 * Get petition data for the current page
 * Can be called directly from PHP content files
 */
function petitionGetPageData(?Context $ctx): ?array {
    static $cache = null;

    // Return cached result if already computed
    if ($cache !== null) {
        return $cache;
    }

    if (!$ctx) {
        return null;
    }

    // Resolve the actual page directory from URL slugs
    $pageDir = petitionResolvePageDir($ctx);
    if (!$pageDir) {
        return null;
    }

    $metadata = loadMetadata($pageDir);

    // Only run if petition-form plugin is enabled
    $plugins = $metadata['plugins'] ?? '';
    if (strpos($plugins, 'petition-form') === false) {
        return null;
    }

    // Use petition_id from metadata if set, otherwise derive from URL path
    $petitionId = $metadata['petition_id'] ?? petitionGetIdFromPath($pageDir);
    if (!$petitionId) {
        return null;
    }

    $csvPath = petitionGetCsvPath($petitionId);
    // loadMetadata() already merges language-specific metadata via Hook::PROCESS_CONTENT
    // Use petition_title if set (for subpages), otherwise fall back to page title
    $petitionTitle = $metadata['petition_title'] ?? $metadata['title'] ?? $petitionId;
    $thankYouPage = $metadata['thank_you_page'] ?? 'takk';
    $newsletterListUuids = !empty($metadata['newsletter_list_uuids'])
        ? array_map('trim', explode(',', $metadata['newsletter_list_uuids']))
        : [];

    $formErrors = [];
    $formData = ['firstname' => '', 'surname' => '', 'email' => '', 'region' => '', 'display' => 'semi'];
    $showForm = true;
    $confirmMessage = null;

    // Check for errors and form data from session (Post/Redirect/Get pattern)
    if (isset($_SESSION['petition_errors'])) {
        $formErrors = $_SESSION['petition_errors'];
        unset($_SESSION['petition_errors']);
    }
    if (isset($_SESSION['petition_form_data'])) {
        $formData = array_merge($formData, $_SESSION['petition_form_data']);
        unset($_SESSION['petition_form_data']);
    }

    // Handle confirmation (GET request with ?confirm=TOKEN)
    if (isset($_GET['confirm']) && !empty($_GET['confirm'])) {
        $token = preg_replace('/[^a-f0-9]/i', '', $_GET['confirm']);
        $result = petitionConfirmSignature($csvPath, $token);

        switch ($result) {
            case 'success':
                $confirmMessage = ['type' => 'success', 'text' => petitionT($ctx, 'petition', 'confirm_success')];
                // Send thank you email with delete link
                $langPrefix = $ctx->get('langPrefix', '');
                $currentPath = trim($ctx->requestPath, '/');
                $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
                $host = $_SERVER['HTTP_HOST'];
                $deleteUrl = "{$protocol}://{$host}{$langPrefix}/{$currentPath}/?delete={$token}#sign-now";
                petitionSendThankYouEmail($token, $deleteUrl, $petitionTitle, $petitionId, $csvPath, $ctx);
                break;
            case 'already':
                $confirmMessage = ['type' => 'info', 'text' => petitionT($ctx, 'petition', 'confirm_already')];
                break;
            case 'expired':
                $confirmMessage = ['type' => 'error', 'text' => petitionT($ctx, 'petition', 'confirm_expired')];
                break;
            default:
                $confirmMessage = ['type' => 'error', 'text' => petitionT($ctx, 'petition', 'confirm_error')];
        }
    }

    // Handle deletion (GET request with ?delete=TOKEN)
    if (isset($_GET['delete']) && !empty($_GET['delete'])) {
        $token = preg_replace('/[^a-f0-9]/i', '', $_GET['delete']);
        $result = petitionDeleteSignature($csvPath, $token);

        switch ($result) {
            case 'success':
                $confirmMessage = ['type' => 'warning', 'text' => petitionT($ctx, 'petition', 'delete_success')];
                break;
            default:
                $confirmMessage = ['type' => 'error', 'text' => petitionT($ctx, 'petition', 'delete_error')];
        }
    }

    // Handle resend confirmation request (POST from resend page)
    if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['petition_resend'])) {
        $resendEmail = strtolower(trim($_POST['resend_email'] ?? ''));

        // Build petition URL for the result message
        $langPrefix = $ctx->get('langPrefix', '');
        $currentPath = trim($ctx->requestPath, '/');
        $petitionPath = preg_replace('#/(takk|send-bekreftelse-pa-nytt)$#', '', $currentPath);
        $petitionUrl = "{$langPrefix}/{$petitionPath}/#sign-now";

        // Rate limit check (reuse existing IP rate limiting)
        if (!petitionCheckIPRateLimit($petitionId . '-resend', 3, 300)) {
            $confirmMessage = ['type' => 'error', 'text' => petitionT($ctx, 'petition', 'resend_rate_limit')];
        } elseif (empty($resendEmail) || !filter_var($resendEmail, FILTER_VALIDATE_EMAIL)) {
            $confirmMessage = ['type' => 'error', 'text' => petitionT($ctx, 'petition', 'email_required')];
        } else {
            // Look up signature by email and attempt to resend if pending
            $signature = petitionGetPendingSignatureByEmail($csvPath, $resendEmail);

            if ($signature !== null && $signature['status'] === 'pending') {
                // Generate new token and send email
                $newToken = bin2hex(random_bytes(32));
                if (petitionUpdateSignatureToken($csvPath, $resendEmail, $newToken)) {
                    $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
                    $host = $_SERVER['HTTP_HOST'];
                    $confirmUrl = "{$protocol}://{$host}{$langPrefix}/{$petitionPath}/?confirm={$newToken}#sign-now";

                    $signatureData = [
                        'email' => $signature['email'],
                        'firstname' => $signature['firstname'],
                        'surname' => $signature['surname']
                    ];

                    // Send email (ignore result - show same message regardless)
                    petitionSendConfirmationEmail($signatureData, $confirmUrl, $petitionTitle, $petitionId, $ctx);
                }
            }

            // Always show the same generic message (privacy: don't reveal if email exists)
            $resultText = petitionT($ctx, 'petition', 'resend_result', ['petition_url' => $petitionUrl]);
            $confirmMessage = ['type' => 'info', 'text' => $resultText, 'html' => true];
        }

        // Store message in session and redirect back (PRG pattern)
        $_SESSION['petition_resend_message'] = $confirmMessage;
        header("Location: {$langPrefix}/{$currentPath}/");
        exit;
    }

    // Handle form submission
    if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['petition_submit'])) {

        // CSRF validation
        if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
            $formErrors[] = petitionT($ctx, 'petition', 'error_csrf');
        }

        // Honeypot check
        if (!empty($_POST['website'])) {
            $formErrors[] = petitionT($ctx, 'petition', 'error_honeypot');
        }

        // Time-based check
        $formStartTime = isset($_POST['form_start_time']) ? (int)$_POST['form_start_time'] : 0;
        if (time() - $formStartTime < 3) {
            $formErrors[] = petitionT($ctx, 'petition', 'error_time_check');
        }

        // Referrer check
        if (!empty($_SERVER['HTTP_REFERER'])) {
            $referrer = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
            $currentHost = $_SERVER['HTTP_HOST'];
            if ($referrer && $referrer !== $currentHost && $referrer !== 'localhost') {
                $formErrors[] = petitionT($ctx, 'petition', 'error_referrer');
            }
        }

        // GDPR Consent validation
        if (!isset($_POST['gdpr_consent']) || $_POST['gdpr_consent'] !== 'on') {
            $formErrors[] = petitionT($ctx, 'petition', 'gdpr_consent_required');
        }

        // Session-based rate limiting (prevents rapid resubmits)
        $lastSubmitTime = $_SESSION['last_petition_submit'] ?? 0;
        if (time() - $lastSubmitTime < 60) {
            $formErrors[] = petitionT($ctx, 'petition', 'error_rate_limit');
        }

        // IP-based rate limiting (prevents email bombing and spam)
        // Max 3 attempts per 5 minutes per IP
        if (empty($formErrors) && !petitionCheckIPRateLimit($petitionId, 3, 300)) {
            $formErrors[] = petitionT($ctx, 'petition', 'error_ip_rate_limit');
        }

        // Get form data with normalization
        $formData['firstname'] = trim($_POST['firstname'] ?? '');
        $formData['surname'] = trim($_POST['surname'] ?? '');
        $formData['email'] = strtolower(trim($_POST['email'] ?? ''));  // Normalize email to lowercase
        $formData['region'] = trim($_POST['region'] ?? '');
        $formData['display'] = trim($_POST['display'] ?? 'semi');
        $formData['gdpr_consent'] = $_POST['gdpr_consent'] ?? '';

        // Unicode normalization for names (if available)
        if (function_exists('normalizer_normalize')) {
            $formData['firstname'] = normalizer_normalize($formData['firstname'], Normalizer::FORM_C);
            $formData['surname'] = normalizer_normalize($formData['surname'], Normalizer::FORM_C);
        }

        // Validation
        if (empty($formData['firstname']) || strlen($formData['firstname']) < 2) {
            $formErrors[] = petitionT($ctx, 'petition', 'firstname_required');
        } elseif (strlen($formData['firstname']) > 50) {
            $formErrors[] = petitionT($ctx, 'petition', 'firstname_required');
        }

        if (empty($formData['surname']) || strlen($formData['surname']) < 2) {
            $formErrors[] = petitionT($ctx, 'petition', 'surname_required');
        } elseif (strlen($formData['surname']) > 50) {
            $formErrors[] = petitionT($ctx, 'petition', 'surname_required');
        }

        if (empty($formData['email']) || !filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) {
            $formErrors[] = petitionT($ctx, 'petition', 'email_required');
        } elseif (strlen($formData['email']) > 100) {
            $formErrors[] = petitionT($ctx, 'petition', 'email_required');
        }

        // Validate region against allowed list
        $validRegions = array_keys(petitionGetRegions($ctx));
        if (empty($formData['region']) || !in_array($formData['region'], $validRegions)) {
            $formErrors[] = petitionT($ctx, 'petition', 'region_required');
        }

        // Validate display preference
        if (!in_array($formData['display'], ['anonymous', 'semi', 'full'])) {
            $formData['display'] = 'semi';
        }

        // Note: Duplicate email check now happens inside petitionAppendSignature()
        // to prevent race conditions with file locking

        // If valid, save and send confirmation email
        if (empty($formErrors)) {
            $token = bin2hex(random_bytes(32));
            $ipHash = hash('sha256', $_SERVER['REMOTE_ADDR'] . $petitionId);
            $currentTime = time();

            $signatureData = [
                'timestamp' => $currentTime,
                'email' => $formData['email'],
                'firstname' => $formData['firstname'],
                'surname' => $formData['surname'],
                'region' => $formData['region'],
                'display' => $formData['display'],
                'status' => 'pending',
                'token' => $token,
                'token_created' => $currentTime,
                'ip_hash' => $ipHash
            ];

            $appendResult = petitionAppendSignature($csvPath, $signatureData);

            if ($appendResult) {
                // Build confirmation URL
                $langPrefix = $ctx->get('langPrefix', '');
                $currentPath = trim($ctx->requestPath, '/');
                $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
                $host = $_SERVER['HTTP_HOST'];
                $confirmUrl = "{$protocol}://{$host}{$langPrefix}/{$currentPath}/?confirm={$token}#sign-now";

                // Send confirmation email
                if (petitionSendConfirmationEmail($signatureData, $confirmUrl, $petitionTitle, $petitionId, $ctx)) {
                    // Subscribe to newsletter if opted in (fire and forget - don't block petition)
                    $newsletterOptIn = isset($_POST['newsletter']) && $_POST['newsletter'] === 'on';
                    error_log("Newsletter checkbox: " . ($newsletterOptIn ? 'checked' : 'not checked'));
                    if ($newsletterOptIn) {
                        $fullName = $formData['firstname'] . ' ' . $formData['surname'];
                        petitionSubscribeToNewsletter($formData['email'], $fullName, $newsletterListUuids);
                    }

                    $_SESSION['last_petition_submit'] = time();
                    $_SESSION['petition_subscribed_newsletter'] = $newsletterOptIn;

                    // Regenerate session ID to prevent session fixation
                    session_regenerate_id(true);

                    // Redirect to thank you page
                    $thankYouUrl = "{$langPrefix}/{$currentPath}/{$thankYouPage}/";
                    header("Location: {$thankYouUrl}");
                    exit;
                } else {
                    $formErrors[] = petitionT($ctx, 'petition', 'error_email_send');
                }
            } else {
                // Append failed - likely due to duplicate email (checked inside lock)
                $formErrors[] = petitionT($ctx, 'petition', 'error_already_signed');
            }
        }

        // If there are errors, store in session and redirect to show them
        if (!empty($formErrors)) {
            $_SESSION['petition_errors'] = $formErrors;
            $_SESSION['petition_form_data'] = $formData;

            $langPrefix = $ctx->get('langPrefix', '');
            $currentPath = trim($ctx->requestPath, '/');
            $redirectUrl = "{$langPrefix}/{$currentPath}/#sign-now";
            header("Location: {$redirectUrl}");
            exit;
        }
    }

    // Get confirmed signatures
    $signatures = petitionGetConfirmedSignatures($csvPath);

    // Build HTML
    $formHtml = '';

    // Show confirmation message if present
    if ($confirmMessage) {
        $formHtml .= '<div class="form-message form-message--' . $confirmMessage['type'] . '" role="alert">';
        $formHtml .= '<p>' . htmlspecialchars($confirmMessage['text']) . '</p>';
        $formHtml .= '</div>';
    }

    $formHtml .= petitionRenderForm($ctx, $formData, $formErrors, $showForm);
    $signaturesHtml = petitionRenderSignatures($signatures, $ctx);

    // Cache and return results
    $cache = [
        'count' => count($signatures),
        'form' => $formHtml,
        'signatures' => $signaturesHtml
    ];

    return $cache;
}

// --- Main Hook (for TEMPLATE_VARS) ---

Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
    $data = petitionGetPageData($ctx);

    if ($data) {
        $vars['petition_count'] = $data['count'];
        $vars['petition_form'] = $data['form'];
        $vars['petition_signatures'] = $data['signatures'];
    }

    // Check for newsletter subscription flag (for thank you page)
    if (isset($_SESSION['petition_subscribed_newsletter'])) {
        $vars['petition_subscribed_newsletter'] = $_SESSION['petition_subscribed_newsletter'];
        unset($_SESSION['petition_subscribed_newsletter']);
    }

    // Check for resend confirmation message (for thank you page)
    if (isset($_SESSION['petition_resend_message'])) {
        $vars['petition_resend_message'] = $_SESSION['petition_resend_message'];
        unset($_SESSION['petition_resend_message']);
    }

    return $vars;
});
