<?php
// Enhanced redirectTo function to use safeRedirect for better header management.

// Include configuration first to make constants available
// Ensure it's included only once if functions.php itself might be included multiple times,
// though typically it's via header.php using include_once.
// Assuming SITE_NAME is a reliable check for config loaded.
if (!defined('SITE_NAME')) {
    require_once __DIR__ . '/../config/config.php';
}

ini_set('session.use_strict_mode', '1');

// Initialize session using SessionManager if not already initialized
if (!class_exists('SessionManager')) {
    require_once __DIR__ . '/session_manager.php';
}
SessionManager::initialize();

// Define notification settings globally for use in notify_relevant_users and potentially other functions
global $notification_settings_definition;
$notification_settings_definition = [
    'notify_new_user_registration' => ['label' => 'New User Registration', 'admin_only' => true],
    'notify_ticket_creation' => ['label' => 'New Ticket Created', 'admin_only' => true],
    'notify_ticket_status_change' => ['label' => 'Ticket Status Change', 'admin_only' => false], // Admins get all, users if they are involved.
    'notify_ticket_comment' => ['label' => 'New Ticket Comment', 'admin_only' => false],
    'notify_project_task_creation' => ['label' => 'Project Task Created', 'admin_only' => true],
    'notify_project_task_milestone_change' => ['label' => 'Project Task Milestone Change', 'admin_only' => false],
    'notify_project_task_completion' => ['label' => 'Project Task Completed', 'admin_only' => false],
    'notify_project_file_upload' => ['label' => 'Project File Uploaded', 'admin_only' => true]
];

/**
 * Authentication functions
 */
function isLoggedIn()
{
    return isset($_SESSION['user_id']);
}

function isAdmin()
{
    return isLoggedIn() && $_SESSION['role'] === 'admin';
}

function isStaff()
{
    return isLoggedIn() && $_SESSION['role'] === 'staff';
}

function isAdminOrStaff()
{
    return isLoggedIn() && ($_SESSION['role'] === 'admin' || $_SESSION['role'] === 'staff');
}

/**
 * Redirect to a specified URL
 */
function redirectTo($url)
{
    // Include headers manager if not already included
    if (!function_exists('safeRedirect')) {
        require_once __DIR__ . '/headers_manager.php';
    }

    $url = getCleanUrl($url);

    // Use safe redirect function that handles output buffering
    safeRedirect($url);
    exit; // This is redundant but safe
}

/**
 * Get a clean URL for links
 */
function getCleanUrl($page, $params = [])
{
    // Get BASE_URL from configuration with trailing slashes removed
    $base = rtrim(BASE_URL, '/');
    $url = $page;

    // If it's already a full URL, return it as is
    if (preg_match('~^(?:f|ht)tps?://~i', $url)) {
        return $url;
    }

    // Remove .php extension if present
    if (substr($url, -4) === '.php') {
        $url = substr($url, 0, -4);
    }

    // Special case for index - make it the root
    if ($url === 'index') {
        $url = '';
    }

    // Ensure URL has leading slash if not empty
    if (!empty($url) && $url[0] !== '/') {
        $url = '/' . $url;
    }

    // Construct full URL
    $fullUrl = $base . $url;

    // Add query string if params are provided
    if (!empty($params)) {
        $fullUrl .= '?' . http_build_query($params);
    }

    // Force the use of the configured BASE_URL - prevent any host/protocol overriding
    // This ensures links always use the defined BASE_URL in production
    $parsedUrl = parse_url($fullUrl);
    $configUrl = parse_url($base);

    // If we have different hosts/protocols between config and generated URL, use config
    if (
        isset($configUrl['host']) && isset($parsedUrl['host']) &&
        ($configUrl['host'] !== $parsedUrl['host'] ||
            (isset($configUrl['scheme']) && isset($parsedUrl['scheme']) &&
                $configUrl['scheme'] !== $parsedUrl['scheme']))
    ) {

        // Reconstruct using BASE_URL components
        $path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '';
        $query = isset($parsedUrl['query']) ? '?' . $parsedUrl['query'] : '';
        $fullUrl = $base . $path . $query;
    }

    // Log URL construction for debugging
    debugUrl("Generated URL: $fullUrl from page: $page and base: $base");

    return $fullUrl;
}

/**
 * File handling functions
 */
function uploadFile($file, $destination)
{
    if (empty($file) || empty($file['name'])) {
        return '';
    }

    // Standard PHP upload error check
    if ($file['error'] !== UPLOAD_ERR_OK) {
        return false;
    }

    // Validate file size using config constant
    if ($file['size'] > MAX_UPLOAD_SIZE) {
        return false;
    }

    // Validate file extension using config constant
    $fileExtension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    if (!in_array($fileExtension, ALLOWED_EXTENSIONS)) {
        return false;
    }

    // Determine target directory relative to this file
    $uploadDir = __DIR__ . '/../' . rtrim($destination, '/') . '/';

    // Ensure directory exists and is writable
    if (!is_dir($uploadDir)) {
        @mkdir($uploadDir, 0777, true);
    }
    @chmod($uploadDir, 0777);

    // Generate safe filename
    $safeBasename = preg_replace('/[^a-zA-Z0-9_.-]/', '', basename($file['name']));
    $filename = time() . '_' . uniqid() . '_' . $safeBasename;
    $target = $uploadDir . $filename;

    // Perform upload
    if (move_uploaded_file($file['tmp_name'], $target)) {
        @chmod($target, 0644);
        return $filename;
    }

    return false;
}

/**
 * Security functions
 */
function sanitizeInput($data)
{
    $data = trim($data);
    $data = stripslashes($data);
    $data = htmlspecialchars($data, ENT_COMPAT, 'UTF-8');
    return $data;
}

function validateHoneypot()
{
    // If the honeypot field is filled in, it's likely a bot
    if (isset($_POST[HONEYPOT_FIELD]) && !empty($_POST[HONEYPOT_FIELD])) {
        // Bot detected
        return false;
    }
    return true;
}

function generateHoneypotField()
{
    return '<div style="' . HONEYPOT_CSS . '">
        <label for="' . HONEYPOT_FIELD . '">Leave this field empty</label>
        <input type="text" name="' . HONEYPOT_FIELD . '" id="' . HONEYPOT_FIELD . '">
    </div>';
}

/**
 * hCaptcha validation and generation functions
 */
function validateHCaptcha($token)
{
    // Skip validation if hCaptcha is disabled
    if (!defined('HCAPTCHA_ENABLED') || !HCAPTCHA_ENABLED) {
        return true;
    }

    // Skip validation if no token provided and hCaptcha is enabled
    if (empty($token)) {
        return false;
    }

    // Development mode: Use test keys automatically
    $secret_key = HCAPTCHA_SECRET_KEY;
    $is_development = (defined('DEBUG_MODE') && DEBUG_MODE) ||
        (isset($_SERVER['HTTP_HOST']) &&
            (strpos($_SERVER['HTTP_HOST'], 'localhost') !== false ||
                strpos($_SERVER['HTTP_HOST'], '127.0.0.1') !== false ||
                strpos($_SERVER['HTTP_HOST'], '.local') !== false));

    if ($is_development && (empty($secret_key) || $secret_key === '')) {
        // Use test secret key for development
        $secret_key = '0x0000000000000000000000000000000000000000';
        error_log("hCaptcha: Using test secret key for development environment");
    }

    // Skip validation if no secret key configured
    if (empty($secret_key)) {
        error_log("hCaptcha validation skipped: SECRET_KEY not configured");
        return true; // Don't block users if misconfigured
    }

    try {
        $verify_url = 'https://api.hcaptcha.com/siteverify';

        $data = [
            'secret' => $secret_key,
            'response' => $token,
            'remoteip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
        ];

        // Use cURL for the API request
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $verify_url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);

        $response = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curl_error = curl_error($ch);
        $curl_errno = curl_errno($ch);
        curl_close($ch);

        // Comprehensive error checking
        if ($response === false) {
            error_log("hCaptcha API request failed: cURL error " . $curl_errno . " - " . sanitizeLogMessage($curl_error));
            return true; // Don't block users if API is down
        }

        if ($http_code !== 200) {
            error_log("hCaptcha API request failed: HTTP $http_code");
            return true; // Don't block users if API is down
        }

        if (!empty($curl_error)) {
            error_log("hCaptcha API request warning: " . sanitizeLogMessage($curl_error));
        }

        $response_data = json_decode($response, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            error_log("hCaptcha API response parsing error: " . json_last_error_msg());
            return true; // Don't block users if response is malformed
        }

        // Log failed validations for debugging
        if (!isset($response_data['success']) || !$response_data['success']) {
            $error_codes = isset($response_data['error-codes']) ? implode(', ', $response_data['error-codes']) : 'unknown';
            error_log("hCaptcha validation failed: " . $error_codes);
            return false;
        }

        return true;

    } catch (Exception $e) {
        error_log("hCaptcha validation exception: " . $e->getMessage());
        return true; // Don't block users if there's an exception
    }
}

function generateHCaptchaField()
{
    // Return empty string if hCaptcha is disabled
    if (!defined('HCAPTCHA_ENABLED') || !HCAPTCHA_ENABLED) {
        return '';
    }

    // Development mode: Use test keys automatically
    $site_key = HCAPTCHA_SITE_KEY;
    $is_development = (defined('DEBUG_MODE') && DEBUG_MODE) ||
        (isset($_SERVER['HTTP_HOST']) &&
            (strpos($_SERVER['HTTP_HOST'], 'localhost') !== false ||
                strpos($_SERVER['HTTP_HOST'], '127.0.0.1') !== false ||
                strpos($_SERVER['HTTP_HOST'], '.local') !== false));

    if ($is_development && (empty($site_key) || $site_key === '')) {
        // Use test site key for development
        $site_key = '10000000-ffff-ffff-ffff-000000000001';
        error_log("hCaptcha: Using test site key for development environment");
    }

    // Return empty string if no site key configured
    if (empty($site_key)) {
        return '';
    }

    return '<div class="h-captcha" data-sitekey="' . htmlspecialchars($site_key, ENT_QUOTES, 'UTF-8') . '"></div>';
}

function prepareStatement($pdo, $sql, $params = [])
{
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    return $stmt;
}

function getFormattedDate($date, $format = null)
{
    return formatDateForDisplay($date, $format);
}

function displayAlert($message, $type = 'success', $is_html = false, $auto_dismiss = null)
{
    // Auto-dismiss success and info messages by default
    if ($auto_dismiss === null) {
        $auto_dismiss = in_array($type, ['success', 'info']);
    }

    // Generate unique ID for alert management
    $alert_id = 'alert_' . uniqid();

    $processed_message = $is_html ? $message : htmlspecialchars($message, ENT_QUOTES, 'UTF-8');

    // Add appropriate Bootstrap icons based on alert type
    $icon = '';
    switch ($type) {
        case 'success':
            $icon = '<i class="bi bi-check-circle-fill me-2"></i>';
            break;
        case 'danger':
        case 'error':
            $icon = '<i class="bi bi-exclamation-triangle-fill me-2"></i>';
            break;
        case 'warning':
            $icon = '<i class="bi bi-exclamation-circle-fill me-2"></i>';
            break;
        case 'info':
            $icon = '<i class="bi bi-info-circle-fill me-2"></i>';
            break;
        case 'primary':
            $icon = '<i class="bi bi-info-circle me-2"></i>';
            break;
        default:
            $icon = '<i class="bi bi-bell-fill me-2"></i>';
            break;
    }

    // Enhanced alert with auto-dismiss functionality and icons
    $alert_html = "<div id='{$alert_id}' class='alert alert-{$type} alert-dismissible fade show alert-enhanced' role='alert' data-auto-dismiss='{$auto_dismiss}'>
                <div class='alert-content'>
                    {$icon}{$processed_message}
                </div>
                <button type='button' class='btn-close' data-bs-dismiss='alert' aria-label='Close'></button>
            </div>";

    // Add JavaScript for auto-dismiss and stacking prevention
    $alert_html .= "
    <script>
    document.addEventListener('DOMContentLoaded', function() {
        const alertElement = document.getElementById('{$alert_id}');
        if (alertElement) {
            // Remove existing alerts of the same type to prevent stacking
            const existingAlerts = document.querySelectorAll('.alert-{$type}:not(#{$alert_id})');
            existingAlerts.forEach(function(alert) {
                if (alert.parentNode) {
                    alert.remove();
                }
            });
            
            // Auto-dismiss functionality
            if (alertElement.dataset.autoDismiss === 'true') {
                setTimeout(function() {
                    if (alertElement && alertElement.parentNode) {
                        const bsAlert = new bootstrap.Alert(alertElement);
                        bsAlert.close();
                    }
                }, " . ($type === 'success' ? '5000' : '7000') . ");
            }
            
            // Add smooth animation
            alertElement.style.opacity = '0';
            alertElement.style.transform = 'translateY(-20px)';
            setTimeout(function() {
                alertElement.style.transition = 'all 0.3s ease-in-out';
                alertElement.style.opacity = '1';
                alertElement.style.transform = 'translateY(0)';
            }, 50);
        }
    });
    </script>";

    return $alert_html;
}

/**
 * Task priority functions
 */
function getTaskPriorityLabel($priority)
{
    $priorities = TASK_PRIORITIES;
    return isset($priorities[$priority]) ? $priorities[$priority] : 'Medium';
}

function getTaskPriorityBadge($priority)
{
    $class = 'bg-secondary';

    switch ($priority) {
        case 'low':
            $class = 'bg-info';
            break;
        case 'medium':
            $class = 'bg-primary';
            break;
        case 'high':
            $class = 'bg-warning text-dark';
            break;
        case 'urgent':
            $class = 'bg-danger';
            break;
    }

    return '<span class="badge ' . $class . '">' . getTaskPriorityLabel($priority) . '</span>';
}

/**
 * Get the base URL for the application
 */
function getBaseUrl()
{
    return rtrim(BASE_URL, '/');
}

/**
 * Debug function to help troubleshoot URL issues
 */
function debugUrl($message)
{
    if (defined('DEBUG_MODE') && DEBUG_MODE) {
        error_log("[URL Debug] " . $message);
    }
}

/**
 * CSRF Token functions
 */
function generate_csrf_token()
{
    // Ensure session is initialized
    if (!SessionManager::isInitialized()) {
        SessionManager::initialize();
    }
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

function validate_csrf_token($token_from_form)
{
    // Ensure session is initialized
    if (!SessionManager::isInitialized()) {
        SessionManager::initialize();
    }
    if (!isset($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) {
        return false;
    }
    if (!is_string($token_from_form)) {
        return false;
    }
    $token_from_form = trim($token_from_form);
    if ($token_from_form === '') {
        return false;
    }
    return hash_equals($_SESSION['csrf_token'], $token_from_form);
}

function get_csrf_input_field()
{
    $token = generate_csrf_token();
    return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($token, ENT_QUOTES, 'UTF-8') . '">';
}

/**
 * Enhanced date management functions
 */
function validateDate($date, $format = 'Y-m-d H:i:s')
{
    if (empty($date))
        return false;
    try {
        $d = DateTime::createFromFormat($format, $date, new DateTimeZone(TIMEZONE));
        return $d && $d->format($format) === $date;
    } catch (Exception $e) {
        return false;
    }
}

/**
 * Fetches a specific notification preference for a user.
 * Defaults to true if the setting is not found or the user has no record.
 *
 * @param int $user_id The ID of the user.
 * @param string $preference_key The key of the notification preference (column name).
 * @return bool True if the user should receive the notification, false otherwise.
 */
function get_user_notification_preference(int $user_id, string $preference_key): bool
{
    global $pdo; // Use the global PDO object

    if (!$pdo) {
        error_log("get_user_notification_preference: PDO object is not available.");
        return true; // Default to true if DB connection fails, to be safe (or handle error differently)
    }

    // Validate preference key against a known list of valid keys to prevent SQL injection if key comes from unsafe source
    // For this application, keys are hardcoded, so direct use is acceptable but good practice for future.
    $valid_keys = [
        'notify_new_user_registration',
        'notify_ticket_creation',
        'notify_ticket_status_change',
        'notify_ticket_comment',
        'notify_project_task_creation',
        'notify_project_task_milestone_change',
        'notify_project_task_completion',
        'notify_project_file_upload'
    ];
    if (!in_array($preference_key, $valid_keys)) {
        error_log("get_user_notification_preference: Invalid preference key '{$preference_key}'.");
        return false; // Or true, depending on desired default for invalid keys
    }

    try {
        $stmt = $pdo->prepare("SELECT {$preference_key} FROM user_notification_settings WHERE user_id = ?");
        $stmt->execute([$user_id]);
        $setting = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($setting === false) {
            // No record found for the user, default to true (as per table schema defaults)
            return true;
        }
        return (bool) $setting[$preference_key];
    } catch (PDOException $e) {
        error_log("PDOException in get_user_notification_preference for user_id {$user_id}, key {$preference_key}: " . $e->getMessage());
        return true; // Default to true on error
    }
}

/**
 * Fetches all admin users.
 *
 * @return array An array of admin user objects (id, email, name), or empty array on failure.
 */
function get_admin_users(): array
{
    global $pdo;
    if (!$pdo) {
        error_log("get_admin_users: PDO object is not available.");
        return [];
    }
    try {
        $stmt = $pdo->prepare("SELECT id, email, name FROM users WHERE role IN ('admin', 'staff')");
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    } catch (PDOException $e) {
        error_log("PDOException in get_admin_users: " . $e->getMessage());
        return [];
    }
}

/**
 * Sends notifications to relevant users based on their preferences.
 *
 * @param string $preference_key The notification type key (e.g., 'notify_ticket_creation').
 * @param string $subject The email subject.
 * @param string $body The HTML email body.
 * @param array $specific_user_ids An array of user IDs to specifically notify (e.g., ticket creator).
 * @param bool $is_admin_event If true, targets all admins. If false, targets only $specific_user_ids.
 *                             Both are subject to the user's $preference_key.
 * @return void
 */
function notify_relevant_users(string $preference_key, string $subject, string $body, array $specific_user_ids = [], bool $is_admin_event = true): void
{
    global $pdo, $notification_settings_definition; // Ensure $notification_settings_definition is in scope
    if (!$pdo) {
        error_log("notify_relevant_users: PDO object is not available.");
        return;
    }

    $current_user_id = isset($_SESSION['user_id']) ? (int) $_SESSION['user_id'] : null; // Renamed for clarity
    $recipients = [];
    $processed_user_ids = []; // To avoid duplicate processing

    $admin_self_notification_skip_keys = [
        'notify_project_task_creation',
        'notify_project_task_milestone_change',
        // 'notify_ticket_comment' is handled slightly differently but included for context
    ];

    if ($is_admin_event) {
        $admins = get_admin_users();
        foreach ($admins as $admin) {
            // Skip if admin is the current user AND (it's a ticket comment OR it's a skippable project-related action)
            if (
                $admin['id'] == $current_user_id &&
                ($preference_key === 'notify_ticket_comment' || in_array($preference_key, $admin_self_notification_skip_keys))
            ) {
                $processed_user_ids[] = $admin['id']; // Mark as processed
                continue;
            }

            if (!in_array($admin['id'], $processed_user_ids)) {
                if (get_user_notification_preference($admin['id'], $preference_key)) {
                    $recipients[] = ['email' => $admin['email'], 'name' => $admin['name']];
                }
                $processed_user_ids[] = $admin['id'];
            }
        }
    }

    if (!empty($specific_user_ids)) {
        // Ensure specific_user_ids are integers
        $specific_user_ids = array_map('intval', $specific_user_ids);
        $placeholders = implode(',', array_fill(0, count($specific_user_ids), '?'));

        if (!empty($placeholders)) { // Check if there are any specific users to query
            try {
                $stmt = $pdo->prepare("SELECT id, email, name, role FROM users WHERE id IN ({$placeholders})");
                $stmt->execute($specific_user_ids);
                $users_to_notify = $stmt->fetchAll(PDO::FETCH_ASSOC);

                foreach ($users_to_notify as $user) {
                    // Skip if this user is the current user (actor) AND
                    // (it's a ticket comment OR (it's a skippable project action AND this user is an admin))
                    if (
                        $user['id'] == $current_user_id &&
                        (
                            $preference_key === 'notify_ticket_comment' ||
                            (in_array($preference_key, $admin_self_notification_skip_keys) && in_array($user['role'], ['admin', 'staff']))
                        )
                    ) {
                        $processed_user_ids[] = $user['id']; // Mark as processed
                        continue;
                    }

                    if (!in_array($user['id'], $processed_user_ids)) {
                        $is_pref_admin_only = false;
                        if (isset($notification_settings_definition[$preference_key])) {
                            $is_pref_admin_only = $notification_settings_definition[$preference_key]['admin_only'];
                        }

                        // If this preference type is strictly for admins, and this specific user is not an admin or staff, skip.
                        if ($is_pref_admin_only && !in_array($user['role'], ['admin', 'staff'])) {
                            // Skip this non-admin/staff user for admin-only notifications
                        } else {
                            // Otherwise, check their preference and add if they want it
                            if (get_user_notification_preference($user['id'], $preference_key)) {
                                $recipients[] = ['email' => $user['email'], 'name' => $user['name']];
                            }
                        }
                        $processed_user_ids[] = $user['id'];
                    }
                }
            } catch (PDOException $e) {
                error_log("PDOException in notify_relevant_users while fetching specific users: " . $e->getMessage());
            }
        }
    }

    // Deduplicate recipients by email to avoid sending multiple emails to the same address
    // if an admin is also in specific_user_ids.
    $final_recipients = [];
    $emails_sent = [];
    foreach ($recipients as $recipient) {
        if (!in_array($recipient['email'], $emails_sent)) {
            $final_recipients[] = $recipient;
            $emails_sent[] = $recipient['email'];
        }
    }

    foreach ($final_recipients as $recipient) {
        $email_body_personalized = "Dear " . htmlspecialchars($recipient['name']) . ",<br><br>" . $body;
        if (!send_custom_email($recipient['email'], $subject, $email_body_personalized)) {
            error_log("Failed to send '{$preference_key}' notification to " . $recipient['email']);
        } else {
            // For debugging: error_log("Sent '{$preference_key}' notification to " . $recipient['email']);
        }
    }
}

function makeLinksClickable($text)
{
    // If the text already contains <a ...>, skip linkifying (assume it's HTML from Summernote)
    if (strpos($text, '<a ') !== false) {
        return $text;
    }
    // Regular expression to match URLs
    $urlPattern = '/(https?:\/\/[^\s]+)/';
    // Replace URLs with anchor tags
    $textWithLinks = preg_replace($urlPattern, '<a href="$1" target="_blank">$1</a>', $text);
    return $textWithLinks;
}


/**
 * Translation helper function
 * 
 * @param string $key The translation key
 * @param string $default Fallback value if key is not found
 * @return string The translated string or the key/default if not found
 */
function __($key, $default = '')
{
    global $lang;
    return $lang[$key] ?? ($default ?: $key);
}

// Include mail functions
require_once __DIR__ . '/mail_functions.php';

function formatDateForDatabase($date)
{
    if (empty($date))
        return null;
    try {
        // Assume $date is a string representing time in TIMEZONE, or already a DateTime object
        if (!$date instanceof DateTimeInterface) {
            $dateObj = new DateTime($date, new DateTimeZone(TIMEZONE));
        } else {
            $dateObj = $date;
        }
        // Convert to UTC for database storage
        $dateObj->setTimezone(new DateTimeZone('UTC'));
        return $dateObj->format('Y-m-d H:i:s');
    } catch (Exception $e) {
        error_log("Date formatting error for database: " . $e->getMessage() . " Input: " . (is_string($date) ? $date : 'DateTimeObject'));
        return null;
    }
}

function formatDateForDisplay($date, $format = null)
{
    if (empty($date) || $date === '0000-00-00 00:00:00' || $date === '0000-00-00') {
        return ''; // Return empty for placeholder/invalid dates
    }

    try {
        // Assume $date string from database is in UTC. If it's already a DateTime object, use it directly.
        if (!$date instanceof DateTimeInterface) {
            $dateObj = new DateTime($date, new DateTimeZone('UTC'));
        } else {
            $dateObj = $date;
        }

        // Convert to user's configured TIMEZONE for display
        $dateObj->setTimezone(new DateTimeZone(TIMEZONE));

        // Determine format: if $format is null, try to guess if it's a date or datetime
        $resolvedFormat = $format;
        if ($resolvedFormat === null) {
            // A simple check: if the original string (if it was a string) contains time parts
            $isDateTime = is_string($date) && (strpos($date, ':') !== false);
            $resolvedFormat = $isDateTime ? DATETIME_FORMAT : DATE_FORMAT;
        }

        return $dateObj->format($resolvedFormat);
    } catch (Exception $e) {
        error_log("Date display error: " . $e->getMessage() . " Input: " . (is_string($date) ? $date : 'DateTimeObject') . " Format: " . $format);
        // Fallback to returning the original date string on error, or an empty string if it was an object that failed.
        return is_string($date) ? $date : '';
    }
}

function getCurrentDateTime()
{
    try {
        $dateTime = new DateTime('now', new DateTimeZone(TIMEZONE));
        return $dateTime->format('Y-m-d H:i:s');
    } catch (Exception $e) {
        error_log("Current datetime error: " . $e->getMessage());
        return date('Y-m-d H:i:s');
    }
}

function isValidTimestamp($timestamp)
{
    return ((string) (int) $timestamp === (string) $timestamp)
        && ($timestamp <= PHP_INT_MAX)
        && ($timestamp >= ~PHP_INT_MAX);
}

function isValidDateRange($start_date, $end_date)
{
    try {
        if (empty($start_date) || empty($end_date)) {
            return false;
        }
        $start = new DateTime($start_date, new DateTimeZone(TIMEZONE));
        $end = new DateTime($end_date, new DateTimeZone(TIMEZONE));
        return $start <= $end;
    } catch (Exception $e) {
        error_log("Date range validation error: " . $e->getMessage());
        return false;
    }
}

function getDateDiff($date1, $date2, $format = '%a')
{
    try {
        $d1 = new DateTime($date1, new DateTimeZone(TIMEZONE));
        $d2 = new DateTime($date2, new DateTimeZone(TIMEZONE));
        $interval = $d1->diff($d2);
        return $interval->format($format);
    } catch (Exception $e) {
        error_log("Date difference calculation error: " . $e->getMessage());
        return false;
    }
}

function compareDates($date1, $date2, $operator = '=')
{
    try {
        $d1 = new DateTime($date1, new DateTimeZone(TIMEZONE));
        $d2 = new DateTime($date2, new DateTimeZone(TIMEZONE));

        switch ($operator) {
            case '>':
                return $d1 > $d2;
            case '<':
                return $d1 < $d2;
            case '>=':
                return $d1 >= $d2;
            case '<=':
                return $d1 <= $d2;
            default:
                return $d1 == $d2;
        }
    } catch (Exception $e) {
        error_log("Date comparison error: " . $e->getMessage());
        return false;
    }
}

function get_gravatar($email, $s = 80, $d = 'mp', $r = 'g', $img = false, $atts = array())
{
    // ... existing code ...
}

/**
 * Returns an array of country codes to country names.
 * ISO 3166-1 alpha-2 codes.
 * This list can be expanded or moved to a separate file if it becomes too large.
 * @return array
 */
function get_country_list()
{
    return [
        'AF' => 'Afghanistan',
        'AX' => 'Åland Islands',
        'AL' => 'Albania',
        'DZ' => 'Algeria',
        'AS' => 'American Samoa',
        'AD' => 'Andorra',
        'AO' => 'Angola',
        'AI' => 'Anguilla',
        'AQ' => 'Antarctica',
        'AG' => 'Antigua and Barbuda',
        'AR' => 'Argentina',
        'AM' => 'Armenia',
        'AW' => 'Aruba',
        'AU' => 'Australia',
        'AT' => 'Austria',
        'AZ' => 'Azerbaijan',
        'BS' => 'Bahamas',
        'BH' => 'Bahrain',
        'BD' => 'Bangladesh',
        'BB' => 'Barbados',
        'BY' => 'Belarus',
        'BE' => 'Belgium',
        'BZ' => 'Belize',
        'BJ' => 'Benin',
        'BM' => 'Bermuda',
        'BT' => 'Bhutan',
        'BO' => 'Bolivia',
        'BQ' => 'Bonaire, Sint Eustatius and Saba',
        'BA' => 'Bosnia and Herzegovina',
        'BW' => 'Botswana',
        'BV' => 'Bouvet Island',
        'BR' => 'Brazil',
        'IO' => 'British Indian Ocean Territory',
        'BN' => 'Brunei Darussalam',
        'BG' => 'Bulgaria',
        'BF' => 'Burkina Faso',
        'BI' => 'Burundi',
        'CV' => 'Cabo Verde',
        'KH' => 'Cambodia',
        'CM' => 'Cameroon',
        'CA' => 'Canada',
        'KY' => 'Cayman Islands',
        'CF' => 'Central African Republic',
        'TD' => 'Chad',
        'CL' => 'Chile',
        'CN' => 'China',
        'CX' => 'Christmas Island',
        'CC' => 'Cocos (Keeling) Islands',
        'CO' => 'Colombia',
        'KM' => 'Comoros',
        'CG' => 'Congo',
        'CD' => 'Congo, Democratic Republic of the',
        'CK' => 'Cook Islands',
        'CR' => 'Costa Rica',
        'CI' => 'Côte d\'Ivoire',
        'HR' => 'Croatia',
        'CU' => 'Cuba',
        'CW' => 'Curaçao',
        'CY' => 'Cyprus',
        'CZ' => 'Czechia',
        'DK' => 'Denmark',
        'DJ' => 'Djibouti',
        'DM' => 'Dominica',
        'DO' => 'Dominican Republic',
        'EC' => 'Ecuador',
        'EG' => 'Egypt',
        'SV' => 'El Salvador',
        'GQ' => 'Equatorial Guinea',
        'ER' => 'Eritrea',
        'EE' => 'Estonia',
        'SZ' => 'Eswatini',
        'ET' => 'Ethiopia',
        'FK' => 'Falkland Islands (Malvinas)',
        'FO' => 'Faroe Islands',
        'FJ' => 'Fiji',
        'FI' => 'Finland',
        'FR' => 'France',
        'GF' => 'French Guiana',
        'PF' => 'French Polynesia',
        'TF' => 'French Southern Territories',
        'GA' => 'Gabon',
        'GM' => 'Gambia',
        'GE' => 'Georgia',
        'DE' => 'Germany',
        'GH' => 'Ghana',
        'GI' => 'Gibraltar',
        'GR' => 'Greece',
        'GL' => 'Greenland',
        'GD' => 'Grenada',
        'GP' => 'Guadeloupe',
        'GU' => 'Guam',
        'GT' => 'Guatemala',
        'GG' => 'Guernsey',
        'GN' => 'Guinea',
        'GW' => 'Guinea-Bissau',
        'GY' => 'Guyana',
        'HT' => 'Haiti',
        'HM' => 'Heard Island and McDonald Islands',
        'VA' => 'Holy See',
        'HN' => 'Honduras',
        'HK' => 'Hong Kong',
        'HU' => 'Hungary',
        'IS' => 'Iceland',
        'IN' => 'India',
        'ID' => 'Indonesia',
        'IR' => 'Iran',
        'IQ' => 'Iraq',
        'IE' => 'Ireland',
        'IM' => 'Isle of Man',
        'IL' => 'Israel',
        'IT' => 'Italy',
        'JM' => 'Jamaica',
        'JP' => 'Japan',
        'JE' => 'Jersey',
        'JO' => 'Jordan',
        'KZ' => 'Kazakhstan',
        'KE' => 'Kenya',
        'KI' => 'Kiribati',
        'KP' => 'Korea (Democratic People\'s Republic of)',
        'KR' => 'Korea, Republic of',
        'KW' => 'Kuwait',
        'KG' => 'Kyrgyzstan',
        'LA' => 'Lao People\'s Democratic Republic',
        'LV' => 'Latvia',
        'LB' => 'Lebanon',
        'LS' => 'Lesotho',
        'LR' => 'Liberia',
        'LY' => 'Libya',
        'LI' => 'Liechtenstein',
        'LT' => 'Lithuania',
        'LU' => 'Luxembourg',
        'MO' => 'Macao',
        'MG' => 'Madagascar',
        'MW' => 'Malawi',
        'MY' => 'Malaysia',
        'MV' => 'Maldives',
        'ML' => 'Mali',
        'MT' => 'Malta',
        'MH' => 'Marshall Islands',
        'MQ' => 'Martinique',
        'MR' => 'Mauritania',
        'MU' => 'Mauritius',
        'YT' => 'Mayotte',
        'MX' => 'Mexico',
        'FM' => 'Micronesia (Federated States of)',
        'MD' => 'Moldova, Republic of',
        'MC' => 'Monaco',
        'MN' => 'Mongolia',
        'ME' => 'Montenegro',
        'MS' => 'Montserrat',
        'MA' => 'Morocco',
        'MZ' => 'Mozambique',
        'MM' => 'Myanmar',
        'NA' => 'Namibia',
        'NR' => 'Nauru',
        'NP' => 'Nepal',
        'NL' => 'Netherlands',
        'NC' => 'New Caledonia',
        'NZ' => 'New Zealand',
        'NI' => 'Nicaragua',
        'NE' => 'Niger',
        'NG' => 'Nigeria',
        'NU' => 'Niue',
        'NF' => 'Norfolk Island',
        'MK' => 'North Macedonia',
        'MP' => 'Northern Mariana Islands',
        'NO' => 'Norway',
        'OM' => 'Oman',
        'PK' => 'Pakistan',
        'PW' => 'Palau',
        'PS' => 'Palestine, State of',
        'PA' => 'Panama',
        'PG' => 'Papua New Guinea',
        'PY' => 'Paraguay',
        'PE' => 'Peru',
        'PH' => 'Philippines',
        'PN' => 'Pitcairn',
        'PL' => 'Poland',
        'PT' => 'Portugal',
        'PR' => 'Puerto Rico',
        'QA' => 'Qatar',
        'RE' => 'Réunion',
        'RO' => 'Romania',
        'RU' => 'Russian Federation',
        'RW' => 'Rwanda',
        'BL' => 'Saint Barthélemy',
        'SH' => 'Saint Helena, Ascension and Tristan da Cunha',
        'KN' => 'Saint Kitts and Nevis',
        'LC' => 'Saint Lucia',
        'MF' => 'Saint Martin (French part)',
        'PM' => 'Saint Pierre and Miquelon',
        'VC' => 'Saint Vincent and the Grenadines',
        'WS' => 'Samoa',
        'SM' => 'San Marino',
        'ST' => 'Sao Tome and Principe',
        'SA' => 'Saudi Arabia',
        'SN' => 'Senegal',
        'RS' => 'Serbia',
        'SC' => 'Seychelles',
        'SL' => 'Sierra Leone',
        'SG' => 'Singapore',
        'SX' => 'Sint Maarten (Dutch part)',
        'SK' => 'Slovakia',
        'SI' => 'Slovenia',
        'SB' => 'Solomon Islands',
        'SO' => 'Somalia',
        'ZA' => 'South Africa',
        'GS' => 'South Georgia and the South Sandwich Islands',
        'SS' => 'South Sudan',
        'ES' => 'Spain',
        'LK' => 'Sri Lanka',
        'SD' => 'Sudan',
        'SR' => 'Suriname',
        'SJ' => 'Svalbard and Jan Mayen',
        'SE' => 'Sweden',
        'CH' => 'Switzerland',
        'SY' => 'Syrian Arab Republic',
        'TW' => 'Taiwan, Province of China',
        'TJ' => 'Tajikistan',
        'TZ' => 'Tanzania, United Republic of',
        'TH' => 'Thailand',
        'TL' => 'Timor-Leste',
        'TG' => 'Togo',
        'TK' => 'Tokelau',
        'TO' => 'Tonga',
        'TT' => 'Trinidad and Tobago',
        'TN' => 'Tunisia',
        'TR' => 'Turkey',
        'TM' => 'Turkmenistan',
        'TC' => 'Turks and Caicos Islands',
        'TV' => 'Tuvalu',
        'UG' => 'Uganda',
        'UA' => 'Ukraine',
        'AE' => 'United Arab Emirates',
        'GB' => 'United Kingdom of Great Britain and Northern Ireland',
        'US' => 'United States of America',
        'UM' => 'United States Minor Outlying Islands',
        'UY' => 'Uruguay',
        'UZ' => 'Uzbekistan',
        'VU' => 'Vanuatu',
        'VE' => 'Venezuela (Bolivarian Republic of)',
        'VN' => 'Viet Nam',
        'VG' => 'Virgin Islands (British)',
        'VI' => 'Virgin Islands (U.S.)',
        'WF' => 'Wallis and Futuna',
        'EH' => 'Western Sahara',
        'YE' => 'Yemen',
        'ZM' => 'Zambia',
        'ZW' => 'Zimbabwe',
    ];
}

/**
 * Wrapper function to provide country list specifically for the settings page dropdown.
 * In admin/settings.php, the 'options' for a select can be a callable.
 * This function calls get_country_list() and returns it in the format expected by the settings form.
 * (key => value, where key is country code, value is country name)
 * @return array
 */
function get_country_list_for_settings()
{
    return get_country_list();
}

/**
 * Enhanced form validation functions
 */
function validatePassword($password, $min_length = 8)
{
    $errors = [];

    if (strlen($password) < $min_length) {
        $errors[] = "Password must be at least {$min_length} characters long";
    }

    if (!preg_match('/[A-Z]/', $password)) {
        $errors[] = "Password must contain at least one uppercase letter";
    }

    if (!preg_match('/[a-z]/', $password)) {
        $errors[] = "Password must contain at least one lowercase letter";
    }

    if (!preg_match('/[0-9]/', $password)) {
        $errors[] = "Password must contain at least one number";
    }

    return [
        'valid' => empty($errors),
        'errors' => $errors,
        'strength' => calculatePasswordStrength($password)
    ];
}

function calculatePasswordStrength($password)
{
    $strength = 0;
    $length = strlen($password);

    // Length points
    if ($length >= 8)
        $strength += 1;
    if ($length >= 12)
        $strength += 1;
    if ($length >= 16)
        $strength += 1;

    // Character type points
    if (preg_match('/[a-z]/', $password))
        $strength += 1;
    if (preg_match('/[A-Z]/', $password))
        $strength += 1;
    if (preg_match('/[0-9]/', $password))
        $strength += 1;
    if (preg_match('/[^a-zA-Z0-9]/', $password))
        $strength += 1;

    // Return strength level
    if ($strength >= 6)
        return 'strong';
    if ($strength >= 4)
        return 'medium';
    if ($strength >= 2)
        return 'weak';
    return 'very-weak';
}

function validateEmail($email)
{
    $validation = [
        'valid' => false,
        'errors' => [],
        'sanitized' => ''
    ];

    if (empty($email)) {
        $validation['errors'][] = 'Email address is required';
        return $validation;
    }

    $sanitized = filter_var(trim($email), FILTER_SANITIZE_EMAIL);
    $validation['sanitized'] = $sanitized;

    if (!filter_var($sanitized, FILTER_VALIDATE_EMAIL)) {
        $validation['errors'][] = 'Please enter a valid email address';
        return $validation;
    }

    // Additional email validation rules
    if (strlen($sanitized) > 254) {
        $validation['errors'][] = 'Email address is too long';
        return $validation;
    }

    // Check for banned email providers
    if (defined('BANNED_EMAIL_PROVIDERS') && !empty(BANNED_EMAIL_PROVIDERS)) {
        $email_domain = strtolower(substr(strrchr($sanitized, '@'), 1));
        $banned_providers = array_map('trim', array_map('strtolower', explode(',', BANNED_EMAIL_PROVIDERS)));

        if (in_array($email_domain, $banned_providers)) {
            $validation['errors'][] = 'This email provider is not supported. Use a professional email address';
            return $validation;
        }
    }

    $validation['valid'] = true;
    return $validation;
}

function preserveFormData($fields)
{
    $preserved = [];
    foreach ($fields as $field) {
        if (isset($_POST[$field])) {
            $preserved[$field] = htmlspecialchars($_POST[$field], ENT_QUOTES, 'UTF-8');
        }
    }
    return $preserved;
}

function generateFormValidationScript($form_id, $validation_rules)
{
    $script = "
    <script>
    document.addEventListener('DOMContentLoaded', function() {
        const form = document.getElementById('{$form_id}');
        if (!form) return;
        
        const validationRules = " . json_encode($validation_rules) . ";
        
        // Add real-time validation
        Object.keys(validationRules).forEach(function(fieldName) {
            const field = form.querySelector('[name=\"' + fieldName + '\"]');
            if (field) {
                field.addEventListener('input', function() {
                    validateField(fieldName, field.value, validationRules[fieldName]);
                });
                
                field.addEventListener('blur', function() {
                    validateField(fieldName, field.value, validationRules[fieldName]);
                });
            }
        });
        
        // Form submission validation
        form.addEventListener('submit', function(e) {
            let isValid = true;
            
            Object.keys(validationRules).forEach(function(fieldName) {
                const field = form.querySelector('[name=\"' + fieldName + '\"]');
                if (field) {
                    const fieldValid = validateField(fieldName, field.value, validationRules[fieldName]);
                    if (!fieldValid) isValid = false;
                }
            });
            
            if (!isValid) {
                e.preventDefault();
                showFormError('Please correct the errors below before submitting.');
            }
        });
        
        function validateField(fieldName, value, rules) {
            const field = form.querySelector('[name=\"' + fieldName + '\"]');
            const feedbackDiv = field.parentNode.querySelector('.field-feedback') || createFeedbackDiv(field);
            
            let isValid = true;
            let errors = [];
            
            // Required validation
            if (rules.required && (!value || value.trim() === '')) {
                errors.push(rules.requiredMessage || 'This field is required');
                isValid = false;
            }
            
            // Email validation
            if (rules.type === 'email' && value && !isValidEmailFormat(value)) {
                errors.push('Please enter a valid email address');
                isValid = false;
            }
            
            // Password validation
            if (rules.type === 'password' && value) {
                const passwordValidation = validatePasswordClient(value, rules.minLength || 8);
                if (!passwordValidation.valid) {
                    errors = errors.concat(passwordValidation.errors);
                    isValid = false;
                }
            }
            
            // Update field appearance
            if (isValid) {
                field.classList.remove('is-invalid');
                field.classList.add('is-valid');
                feedbackDiv.className = 'field-feedback valid-feedback';
                feedbackDiv.textContent = '';
            } else {
                field.classList.remove('is-valid');
                field.classList.add('is-invalid');
                feedbackDiv.className = 'field-feedback invalid-feedback';
                feedbackDiv.innerHTML = errors.join('<br>');
            }
            
            return isValid;
        }
        
        function createFeedbackDiv(field) {
            const feedbackDiv = document.createElement('div');
            feedbackDiv.className = 'field-feedback';
            field.parentNode.appendChild(feedbackDiv);
            return feedbackDiv;
        }
        
        function isValidEmailFormat(email) {
            const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
            return emailRegex.test(email);
        }
        
        function validatePasswordClient(password, minLength) {
            const errors = [];
            
            if (password.length < minLength) {
                errors.push('Password must be at least ' + minLength + ' characters long');
            }
            
            if (!/[A-Z]/.test(password)) {
                errors.push('Password must contain at least one uppercase letter');
            }
            
            if (!/[a-z]/.test(password)) {
                errors.push('Password must contain at least one lowercase letter');
            }
            
            if (!/[0-9]/.test(password)) {
                errors.push('Password must contain at least one number');
            }
            
            return {
                valid: errors.length === 0,
                errors: errors
            };
        }
        
        function showFormError(message) {
            // Remove existing form-level error
            const existingError = form.querySelector('.form-error-message');
            if (existingError) {
                existingError.remove();
            }
            
            // Add new error message
            const errorDiv = document.createElement('div');
            errorDiv.className = 'alert alert-danger form-error-message';
            errorDiv.innerHTML = '<i class=\"bi bi-exclamation-triangle-fill\"></i> ' + message;
            form.insertBefore(errorDiv, form.firstChild);
            
            // Scroll to error
            errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
        }
    });
    </script>";

    return $script;
}

/**
 * Rate Limiting Functions for Spam Prevention
 */

/**
 * Check if a user/IP has exceeded rate limits for a specific action
 * @param string $action_type Type of action (ticket_create, register, contact)
 * @param string|null $email Email address (optional)
 * @param int $limit_minutes Time window in minutes (default from config)
 * @param int $max_attempts Maximum attempts allowed (default from config)
 * @return array ['allowed' => bool, 'message' => string, 'time_remaining' => int]
 */
function checkRateLimit($action_type, $email = null, $limit_minutes = null, $max_attempts = null)
{
    global $pdo;

    // Check if rate limiting is enabled
    if (!defined('RATE_LIMIT_ENABLED') || !RATE_LIMIT_ENABLED) {
        return ['allowed' => true, 'message' => '', 'time_remaining' => 0];
    }

    if (!$pdo) {
        error_log("Rate limit check failed: PDO not available - using fallback");
        // Fallback to session-based rate limiting when database is unavailable
        return checkRateLimitFallback($action_type, $email, $limit_minutes, $max_attempts);
    }

    // Use action-specific config if available, otherwise fall back to general config
    if ($limit_minutes === null || $max_attempts === null) {
        switch ($action_type) {
            case 'register':
                $limit_minutes = $limit_minutes ?? (defined('REGISTRATION_RATE_LIMIT_MINUTES') ? REGISTRATION_RATE_LIMIT_MINUTES : RATE_LIMIT_MINUTES);
                $max_attempts = $max_attempts ?? (defined('REGISTRATION_RATE_LIMIT_MAX_ATTEMPTS') ? REGISTRATION_RATE_LIMIT_MAX_ATTEMPTS : RATE_LIMIT_MAX_ATTEMPTS);
                break;
            default:
                // Use general settings for tickets, contact, etc.
                $limit_minutes = $limit_minutes ?? (defined('RATE_LIMIT_MINUTES') ? RATE_LIMIT_MINUTES : 5);
                $max_attempts = $max_attempts ?? (defined('RATE_LIMIT_MAX_ATTEMPTS') ? RATE_LIMIT_MAX_ATTEMPTS : 3);
                break;
        }
    }

    $ip_address = $_SERVER['REMOTE_ADDR'] ?? 'unknown';

    try {
        // Clean old entries (configurable hours) to keep table size manageable
        $cleanup_hours = defined('RATE_LIMIT_CLEANUP_HOURS') ? RATE_LIMIT_CLEANUP_HOURS : 24;
        $cleanup_stmt = $pdo->prepare("DELETE FROM submission_limits WHERE last_submission < DATE_SUB(NOW(), INTERVAL ? HOUR)");
        $cleanup_stmt->execute([$cleanup_hours]);

        // Check IP-based rate limit using database time calculations
        $ip_stmt = $pdo->prepare("SELECT last_submission, attempt_count, 
                                 TIMESTAMPDIFF(MINUTE, last_submission, NOW()) as minutes_passed 
                                 FROM submission_limits 
                                 WHERE ip_address = ? AND action_type = ? 
                                 AND last_submission > DATE_SUB(NOW(), INTERVAL ? MINUTE) 
                                 ORDER BY last_submission DESC LIMIT 1");
        $ip_stmt->execute([$ip_address, $action_type, $limit_minutes]);
        $ip_limit = $ip_stmt->fetch();

        if ($ip_limit && $ip_limit['attempt_count'] >= $max_attempts) {
            $time_remaining = max(0, $limit_minutes - (int) $ip_limit['minutes_passed']);

            return [
                'allowed' => false,
                'message' => "Too many attempts from your IP address. Please wait {$time_remaining} minutes before trying again.",
                'time_remaining' => $time_remaining
            ];
        }

        // Check email-based rate limit (if email provided) using database time calculations
        if ($email) {
            $email_stmt = $pdo->prepare("SELECT last_submission, 
                                        TIMESTAMPDIFF(MINUTE, last_submission, NOW()) as minutes_passed 
                                        FROM submission_limits 
                                        WHERE email = ? AND action_type = ? 
                                        AND last_submission > DATE_SUB(NOW(), INTERVAL ? MINUTE) 
                                        ORDER BY last_submission DESC LIMIT 1");
            $email_stmt->execute([$email, $action_type, $limit_minutes]);
            $email_limit = $email_stmt->fetch();

            if ($email_limit) {
                $time_remaining = max(0, $limit_minutes - (int) $email_limit['minutes_passed']);

                return [
                    'allowed' => false,
                    'message' => "This email address was recently used for this action. Please wait {$time_remaining} minutes before trying again.",
                    'time_remaining' => $time_remaining
                ];
            }
        }

        return ['allowed' => true, 'message' => '', 'time_remaining' => 0];

    } catch (PDOException $e) {
        error_log("Rate limit check error: " . sanitizeLogMessage($e->getMessage()));
        // Fallback to session-based rate limiting when database error occurs
        return checkRateLimitFallback($action_type, $email, $limit_minutes, $max_attempts);
    }
}

/**
 * Fallback rate limiting using session storage when database is unavailable
 * This provides basic protection during database outages
 */
function checkRateLimitFallback($action_type, $email = null, $limit_minutes = null, $max_attempts = null)
{
    // Ensure session is initialized
    if (!SessionManager::isInitialized()) {
        SessionManager::initialize();
    }

    // Use action-specific config if available
    if ($limit_minutes === null || $max_attempts === null) {
        switch ($action_type) {
            case 'register':
                $limit_minutes = $limit_minutes ?? (defined('REGISTRATION_RATE_LIMIT_MINUTES') ? REGISTRATION_RATE_LIMIT_MINUTES : 15);
                $max_attempts = $max_attempts ?? (defined('REGISTRATION_RATE_LIMIT_MAX_ATTEMPTS') ? REGISTRATION_RATE_LIMIT_MAX_ATTEMPTS : 1);
                break;
            default:
                $limit_minutes = $limit_minutes ?? (defined('RATE_LIMIT_MINUTES') ? RATE_LIMIT_MINUTES : 5);
                $max_attempts = $max_attempts ?? (defined('RATE_LIMIT_MAX_ATTEMPTS') ? RATE_LIMIT_MAX_ATTEMPTS : 3);
                break;
        }
    }

    // Initialize rate limit tracking in session
    if (!isset($_SESSION['rate_limit_fallback'])) {
        $_SESSION['rate_limit_fallback'] = [];
    }

    $key = $action_type . '_' . ($email ? md5($email) : ($_SERVER['REMOTE_ADDR'] ?? 'unknown'));
    $now = time();

    // Clean old entries
    if (isset($_SESSION['rate_limit_fallback'][$key])) {
        $last_attempt = $_SESSION['rate_limit_fallback'][$key]['last_attempt'];
        $minutes_passed = ($now - $last_attempt) / 60;

        if ($minutes_passed >= $limit_minutes) {
            // Reset counter if time window has passed
            unset($_SESSION['rate_limit_fallback'][$key]);
        }
    }

    // Check current attempts
    if (isset($_SESSION['rate_limit_fallback'][$key])) {
        $attempts = $_SESSION['rate_limit_fallback'][$key]['attempts'];
        $last_attempt = $_SESSION['rate_limit_fallback'][$key]['last_attempt'];
        $minutes_passed = ($now - $last_attempt) / 60;
        $time_remaining = max(0, ceil($limit_minutes - $minutes_passed));

        if ($attempts >= $max_attempts) {
            return [
                'allowed' => false,
                'message' => "Too many attempts. Please wait {$time_remaining} minutes before trying again.",
                'time_remaining' => $time_remaining
            ];
        }
    }

    return ['allowed' => true, 'message' => '', 'time_remaining' => 0];
}

/**
 * Record a submission attempt for rate limiting
 * @param string $action_type Type of action (ticket_create, register, contact)
 * @param string|null $email Email address (optional)
 */
function recordSubmissionAttempt($action_type, $email = null)
{
    global $pdo;

    if (!$pdo) {
        error_log("Could not record submission attempt: PDO not available");
        return;
    }

    $ip_address = $_SERVER['REMOTE_ADDR'] ?? 'unknown';

    try {
        // Check if there's an existing record for this IP and action within the last hour
        $check_stmt = $pdo->prepare("SELECT id, attempt_count FROM submission_limits 
                                    WHERE ip_address = ? AND action_type = ? 
                                    AND last_submission > DATE_SUB(NOW(), INTERVAL 1 HOUR) 
                                    ORDER BY last_submission DESC LIMIT 1");
        $check_stmt->execute([$ip_address, $action_type]);
        $existing = $check_stmt->fetch();

        if ($existing) {
            // Update existing record
            $update_stmt = $pdo->prepare("UPDATE submission_limits 
                                         SET last_submission = NOW(), 
                                             attempt_count = attempt_count + 1,
                                             email = COALESCE(?, email)
                                         WHERE id = ?");
            $update_stmt->execute([$email, $existing['id']]);
        } else {
            // Insert new record
            $insert_stmt = $pdo->prepare("INSERT INTO submission_limits 
                                         (ip_address, email, action_type, last_submission, attempt_count) 
                                         VALUES (?, ?, ?, NOW(), 1)");
            $insert_stmt->execute([$ip_address, $email, $action_type]);
        }

    } catch (PDOException $e) {
        error_log("Could not record submission attempt: " . sanitizeLogMessage($e->getMessage()));
        // Fallback to session-based tracking
        recordSubmissionAttemptFallback($action_type, $email);
    }
}

/**
 * Fallback submission attempt recording using session storage
 */
function recordSubmissionAttemptFallback($action_type, $email = null)
{
    // Ensure session is initialized
    if (!SessionManager::isInitialized()) {
        SessionManager::initialize();
    }

    if (!isset($_SESSION['rate_limit_fallback'])) {
        $_SESSION['rate_limit_fallback'] = [];
    }

    $key = $action_type . '_' . ($email ? md5($email) : ($_SERVER['REMOTE_ADDR'] ?? 'unknown'));
    $now = time();

    if (isset($_SESSION['rate_limit_fallback'][$key])) {
        $_SESSION['rate_limit_fallback'][$key]['attempts']++;
        $_SESSION['rate_limit_fallback'][$key]['last_attempt'] = $now;
    } else {
        $_SESSION['rate_limit_fallback'][$key] = [
            'attempts' => 1,
            'last_attempt' => $now
        ];
    }
}

/**
 * Get rate limit status for display purposes
 * @param string $action_type Type of action
 * @param string|null $email Email address (optional)
 * @return array Status information
 */
function getRateLimitStatus($action_type, $email = null)
{
    global $pdo;

    if (!$pdo) {
        return ['has_attempts' => false, 'attempts' => 0, 'last_attempt' => null, 'minutes_passed' => 0];
    }

    $ip_address = $_SERVER['REMOTE_ADDR'] ?? 'unknown';

    try {
        $stmt = $pdo->prepare("SELECT attempt_count, last_submission, 
                              TIMESTAMPDIFF(MINUTE, last_submission, NOW()) as minutes_passed 
                              FROM submission_limits 
                              WHERE ip_address = ? AND action_type = ? 
                              AND last_submission > DATE_SUB(NOW(), INTERVAL 1 HOUR) 
                              ORDER BY last_submission DESC LIMIT 1");
        $stmt->execute([$ip_address, $action_type]);
        $result = $stmt->fetch();

        if ($result) {
            return [
                'has_attempts' => true,
                'attempts' => $result['attempt_count'],
                'last_attempt' => $result['last_submission'],
                'minutes_passed' => (int) $result['minutes_passed']
            ];
        }

        return ['has_attempts' => false, 'attempts' => 0, 'last_attempt' => null, 'minutes_passed' => 0];

    } catch (PDOException $e) {
        error_log("Could not get rate limit status: " . $e->getMessage());
        return ['has_attempts' => false, 'attempts' => 0, 'last_attempt' => null, 'minutes_passed' => 0];
    }
}

/**
 * Create an invoice automatically for a successful order
 * @param int $order_id The order ID
 * @param PDO $pdo Database connection
 * @return int|false The invoice ID if successful, false on failure
 */
function createInvoiceForOrder($order_id, $pdo)
{
    try {
        // Start transaction
        $pdo->beginTransaction();

        // Get order details
        $stmt = $pdo->prepare("
            SELECT o.*, u.id as user_account_id
            FROM orders o
            LEFT JOIN users u ON o.user_id = u.id
            WHERE o.id = ?
        ");
        $stmt->execute([$order_id]);
        $order = $stmt->fetch();

        if (!$order) {
            $pdo->rollBack();
            return false;
        }

        // Get order items
        $stmt = $pdo->prepare("
            SELECT oi.*, p.id as product_id
            FROM order_items oi
            LEFT JOIN products p ON oi.product_id = p.id
            WHERE oi.order_id = ?
        ");
        $stmt->execute([$order_id]);
        $order_items = $stmt->fetchAll();

        // Generate invoice number
        $invoice_number = 'INV-' . date('Y') . '-' . str_pad(rand(1, 999999), 6, '0', STR_PAD_LEFT);

        // Ensure invoice number is unique
        $stmt = $pdo->prepare("SELECT COUNT(*) FROM invoices WHERE invoice_number = ?");
        $stmt->execute([$invoice_number]);
        while ($stmt->fetchColumn() > 0) {
            $invoice_number = 'INV-' . date('Y') . '-' . str_pad(rand(1, 999999), 6, '0', STR_PAD_LEFT);
            $stmt->execute([$invoice_number]);
        }

        // Create invoice
        $invoice_status = ($order['payment_status'] === 'paid') ? 'paid' : 'sent';
        $paid_at = ($order['payment_status'] === 'paid') ? date('Y-m-d H:i:s') : null;
        $amount_paid = ($order['payment_status'] === 'paid') ? $order['total_amount'] : 0.00;

        // Set due date - only for unpaid invoices, paid invoices don't need a due date
        $due_date = ($order['payment_status'] === 'paid') ? null : date('Y-m-d', strtotime('+30 days'));

        // Use a default user_id if guest order (required by schema)
        $user_id = $order['user_account_id'] ?: 1; // Fallback to admin user for guest orders

        // Determine if this is a subscription order
        $has_subscription_items = false;
        foreach ($order_items as $item) {
            if ($item['billing_cycle'] !== 'one_time') {
                $has_subscription_items = true;
                break;
            }
        }

        // Create appropriate subject line
        $subject_prefix = $has_subscription_items ? 'Subscription' : 'Purchase';
        $invoice_subject = 'Order #' . $order['order_number'] . ' - ' . $subject_prefix . ' Invoice';

        $stmt = $pdo->prepare("
            INSERT INTO invoices (
                invoice_number, user_id, client_name, client_email, client_company,
                subject, subtotal, tax_rate, tax_amount, total_amount, amount_paid,
                status, due_date, sent_at, paid_at, created_at
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
        ");

        $stmt->execute([
            $invoice_number,
            $user_id,
            $order['client_name'],
            $order['client_email'],
            $order['client_company'],
            $invoice_subject,
            $order['subtotal'] ?? 0,
            $order['tax_rate'] ?? 0,
            $order['tax_amount'] ?? 0,
            $order['total_amount'],
            $amount_paid,
            $invoice_status,
            $due_date, // NULL for paid invoices, date for unpaid ones
            date('Y-m-d H:i:s'), // sent_at
            $paid_at
        ]);

        $invoice_id = $pdo->lastInsertId();

        // Add invoice items
        foreach ($order_items as $item) {
            $stmt = $pdo->prepare("
                INSERT INTO invoice_items (
                    invoice_id, product_id, item_name, description, quantity, 
                    unit_price, total_price
                ) VALUES (?, ?, ?, ?, ?, ?, ?)
            ");

            $description = 'Product Type: ' . ucfirst($item['product_type']);
            if ($item['billing_cycle'] !== 'one_time') {
                $description .= ' | Billing: ' . ucfirst(str_replace('_', ' ', $item['billing_cycle']));
            }

            $stmt->execute([
                $invoice_id,
                $item['product_id'],
                $item['product_name'],
                $description,
                $item['quantity'],
                $item['unit_price'],
                $item['total_price']
            ]);
        }

        // No need to update orders table - we can find invoices by order details
        $pdo->commit();

        error_log("Invoice created successfully for order $order_id: Invoice ID $invoice_id");
        return $invoice_id;

    } catch (Exception $e) {
        $pdo->rollBack();
        error_log("Failed to create invoice for order $order_id: " . $e->getMessage());
        return false;
    }
}

/**
 * Create subscriptions automatically for a paid order containing subscription products
 * @param int $order_id The order ID
 * @param PDO $pdo Database connection
 * @return bool True if subscriptions were created successfully, false on failure
 */
function createSubscriptionsForOrder($order_id, $pdo)
{
    // Check if there's already an active transaction
    $started_own_transaction = false;

    try {
        // Check if there's an active transaction by attempting to start one
        try {
            $pdo->beginTransaction();
            $started_own_transaction = true;
        } catch (PDOException $e) {
            if (strpos($e->getMessage(), 'already an active transaction') !== false) {
                $started_own_transaction = false;
            } else {
                throw $e; // Re-throw if it's a different error
            }
        }

        // Get order details
        $stmt = $pdo->prepare("
            SELECT o.*
            FROM orders o
            WHERE o.id = ? AND o.payment_status = 'paid'
        ");
        $stmt->execute([$order_id]);
        $order = $stmt->fetch();

        if (!$order) {
            if ($started_own_transaction) {
                $pdo->rollBack();
            }
            error_log("Order not found or not paid for subscription creation: $order_id");
            return false;
        }

        // Get subscription items from order
        $stmt = $pdo->prepare("
            SELECT oi.*, p.trial_days, p.setup_fee
            FROM order_items oi
            LEFT JOIN products p ON oi.product_id = p.id
            WHERE oi.order_id = ? AND oi.billing_cycle != 'one_time'
        ");
        $stmt->execute([$order_id]);
        $subscription_items = $stmt->fetchAll();

        if (empty($subscription_items)) {
            if ($started_own_transaction) {
                $pdo->rollBack();
            }
            return false;
        }

        // Get Stripe gateway information
        $stmt = $pdo->prepare("SELECT id, settings, is_test_mode FROM payment_gateways WHERE gateway_key = 'stripe' AND is_enabled = 1");
        $stmt->execute();
        $stripe_gateway = $stmt->fetch();

        if (!$stripe_gateway) {
            if ($started_own_transaction) {
                $pdo->rollBack();
            }
            return false;
        }

        // For each subscription item, create a subscription record
        foreach ($subscription_items as $index => $item) {
            // Calculate dates
            $start_date = date('Y-m-d');
            $trial_end_date = null;
            $next_billing_date = null;

            if ($item['trial_days'] && $item['trial_days'] > 0) {
                $trial_end_date = date('Y-m-d', strtotime("+{$item['trial_days']} days"));
                $next_billing_date = $trial_end_date;
            } else {
                // Calculate next billing date based on billing cycle
                switch ($item['billing_cycle']) {
                    case 'monthly':
                        $next_billing_date = date('Y-m-d', strtotime('+1 month'));
                        break;
                    case 'quarterly':
                        $next_billing_date = date('Y-m-d', strtotime('+3 months'));
                        break;
                    case 'semi_annually':
                        $next_billing_date = date('Y-m-d', strtotime('+6 months'));
                        break;
                    case 'annually':
                        $next_billing_date = date('Y-m-d', strtotime('+1 year'));
                        break;
                    default:
                        $next_billing_date = date('Y-m-d', strtotime('+1 month'));
                }
            }

            // Create subscription record in database
            $stmt = $pdo->prepare("
                INSERT INTO subscriptions (
                    user_id, product_id, order_id, payment_gateway_id, gateway_subscription_id,
                    status, billing_cycle, amount, currency, start_date, next_billing_date, 
                    trial_end_date, gateway_metadata, created_at
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
            ");

            $gateway_metadata = json_encode([
                'stripe_pending' => true, // Flag to indicate Stripe subscription needs to be created
                'order_id' => $order_id,
                'product_id' => $item['product_id']
            ]);

            $subscription_status = ($item['trial_days'] && $item['trial_days'] > 0) ? 'trial' : 'active';
            $user_id = $order['user_id'] ?: null; // Allow NULL for guest orders

            $stmt->execute([
                $user_id,
                $item['product_id'],
                $order_id,
                $stripe_gateway['id'],
                'pending_' . uniqid(), // Temporary ID until Stripe subscription is created
                $subscription_status,
                $item['billing_cycle'],
                $item['unit_price'],
                'USD', // Default currency
                $start_date,
                $next_billing_date,
                $trial_end_date,
                $gateway_metadata
            ]);

            $subscription_id = $pdo->lastInsertId();
        }

        // Only commit if we started our own transaction
        if ($started_own_transaction) {
            $pdo->commit();
        }

        return true;

    } catch (Exception $e) {
        if ($pdo->inTransaction()) {
            $pdo->rollBack();
        }
        error_log("Failed to create subscriptions for order $order_id: " . $e->getMessage());
        return false;
    }
}

/**
 * Link guest subscriptions to a user account based on email
 * @param int $user_id The user ID to link subscriptions to
 * @param string $email The email address to match
 * @param PDO $pdo Database connection
 * @return int Number of subscriptions linked
 */
function linkGuestSubscriptions($user_id, $email, $pdo)
{
    try {
        $stmt = $pdo->prepare("
            UPDATE subscriptions s
            JOIN orders o ON s.order_id = o.id
            SET s.user_id = ?
            WHERE s.user_id IS NULL AND o.client_email = ?
        ");
        $stmt->execute([$user_id, $email]);
        $linked_count = $stmt->rowCount();

        return $linked_count;
    } catch (PDOException $e) {
        error_log("Failed to link guest subscriptions: " . $e->getMessage());
        return 0;
    }
}

/**
 * Fix incomplete subscriptions that have stripe_pending=true
 * This happens when createSubscriptionsForOrder runs but stripe_create_subscription.php fails
 * @param PDO $pdo Database connection
 * @return array Results of the fix operation
 */
function fixIncompleteSubscriptions($pdo)
{
    try {
        // Find subscriptions with stripe_pending=true
        $stmt = $pdo->prepare("
            SELECT s.*, o.client_email, o.client_name, o.order_number
            FROM subscriptions s
            JOIN orders o ON s.order_id = o.id
            WHERE s.gateway_metadata LIKE '%stripe_pending%'
              AND s.gateway_subscription_id LIKE 'pending_%'
        ");
        $stmt->execute();
        $incomplete_subscriptions = $stmt->fetchAll();

        if (empty($incomplete_subscriptions)) {
            return ['fixed' => 0, 'message' => 'No incomplete subscriptions found'];
        }

        $fixed_count = 0;
        foreach ($incomplete_subscriptions as $subscription) {
            // Update gateway_metadata to remove stripe_pending flag and add email info
            $metadata = json_decode($subscription['gateway_metadata'], true);
            $metadata['stripe_pending'] = false;
            $metadata['client_email'] = $subscription['client_email'];
            $metadata['client_name'] = $subscription['client_name'];
            $metadata['order_number'] = $subscription['order_number'];
            $metadata['fix_applied'] = date('Y-m-d H:i:s');

            $stmt_update = $pdo->prepare("
                UPDATE subscriptions 
                SET gateway_metadata = ?
                WHERE id = ?
            ");
            $stmt_update->execute([json_encode($metadata), $subscription['id']]);

            $fixed_count++;

        }

        return [
            'fixed' => $fixed_count,
            'message' => "Fixed $fixed_count incomplete subscriptions",
            'subscriptions' => $incomplete_subscriptions
        ];

    } catch (PDOException $e) {

        return ['fixed' => 0, 'message' => 'Error: ' . $e->getMessage()];
    }
}

/**
 * Create Stripe subscription internally to avoid routing delays
 */
function createStripeSubscriptionInternal($payment_id, $payment_method_id, $csrf_token, $pdo)
{
    try {
        // Debug logging


        // Validate inputs
        if (!$payment_id || !$payment_method_id || !$csrf_token) {
            return ['success' => false, 'error' => 'Missing required parameters'];
        }

        // Validate CSRF token
        if ($csrf_token !== $_SESSION['csrf_token']) {
            return ['success' => false, 'error' => 'Invalid CSRF token'];
        }

        // Helper function to make Stripe API calls
        $makeStripeAPICall = function ($endpoint, $secret_key, $data = [], $method = 'POST') {
            $curl = curl_init();

            $options = [
                CURLOPT_URL => 'https://api.stripe.com/v1/' . $endpoint,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_HTTPHEADER => [
                    'Authorization: Bearer ' . $secret_key,
                    'Content-Type: application/x-www-form-urlencoded'
                ]
            ];

            if ($method === 'POST') {
                $options[CURLOPT_POST] = true;
                if (!empty($data)) {
                    $options[CURLOPT_POSTFIELDS] = http_build_query($data);
                }
            }

            curl_setopt_array($curl, $options);
            $response = curl_exec($curl);
            $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);

            if (curl_error($curl)) {
                curl_close($curl);
                throw new Exception('cURL Error: ' . curl_error($curl));
            }

            curl_close($curl);
            $decoded_response = json_decode($response, true);

            if ($http_code !== 200 && $http_code !== 201) {
                $error_message = isset($decoded_response['error']['message'])
                    ? $decoded_response['error']['message']
                    : 'API Error';
                throw new Exception("Stripe API Error (HTTP $http_code): $error_message");
            }

            return $decoded_response;
        };

        // Get payment details
        $stmt = $pdo->prepare("
            SELECT p.*, o.order_number, o.client_name, o.client_email, o.user_id, o.id as order_id,
                   pg.settings as gateway_settings, pg.is_test_mode, pg.gateway_key
            FROM payments p
            JOIN orders o ON p.order_id = o.id
            JOIN payment_gateways pg ON p.payment_gateway_id = pg.id
            WHERE p.id = ? AND p.status = 'pending' AND pg.gateway_key = 'stripe'
        ");
        $stmt->execute([$payment_id]);
        $payment = $stmt->fetch();

        if (!$payment) {
            return ['success' => false, 'error' => 'Payment not found or invalid'];
        }

        // Parse Stripe settings
        $stripe_settings = json_decode($payment['gateway_settings'], true);
        $is_test_mode = (bool) $payment['is_test_mode'];
        $secret_key = $is_test_mode
            ? ($stripe_settings['test_secret_key'] ?? '')
            : ($stripe_settings['live_secret_key'] ?? '');

        if (empty($secret_key)) {
            return ['success' => false, 'error' => 'Stripe not properly configured'];
        }

        // Start transaction with proper isolation level management
        // Store original isolation level to restore later
        $original_isolation = $pdo->query("SELECT @@SESSION.transaction_isolation")->fetchColumn();
        $pdo->exec("SET autocommit = 0");
        $pdo->beginTransaction();

        // Create Stripe customer
        $customer_data = [
            'email' => $payment['client_email'],
            'name' => $payment['client_name'],
            'payment_method' => $payment_method_id,
            'invoice_settings[default_payment_method]' => $payment_method_id,
            'metadata[user_id]' => $payment['user_id'] ?: 'guest',
            'metadata[order_id]' => $payment['order_id'],
            'metadata[order_number]' => $payment['order_number']
        ];

        $customer = $makeStripeAPICall('customers', $secret_key, $customer_data);


        // Update order status
        $stmt = $pdo->prepare("UPDATE orders SET payment_status = 'paid', status = 'processing', updated_at = NOW() WHERE id = ?");
        $stmt->execute([$payment['order_id']]);

        // Create subscription records
        $subscriptions_created = createSubscriptionsForOrder($payment['order_id'], $pdo);
        if (!$subscriptions_created) {
            throw new Exception('Failed to create subscription records in database');
        }

        // Get created subscription records
        $stmt = $pdo->prepare("
            SELECT s.*, oi.unit_price, oi.quantity, p.name as product_name, p.trial_days
            FROM subscriptions s
            JOIN order_items oi ON s.product_id = oi.product_id AND s.order_id = oi.order_id
            JOIN products p ON s.product_id = p.id
            WHERE s.order_id = ? AND oi.billing_cycle != 'one_time'
        ");
        $stmt->execute([$payment['order_id']]);
        $db_subscriptions = $stmt->fetchAll();

        if (empty($db_subscriptions)) {
            throw new Exception('No subscription records found');
        }

        $created_subscriptions = [];

        // Create Stripe subscriptions
        foreach ($db_subscriptions as $db_sub) {
            // Calculate tax-inclusive price
            $base_unit_price = $db_sub['unit_price'];
            $tax_inclusive_price = $base_unit_price;

            // Get tax settings from order
            $stmt_order_tax = $pdo->prepare("SELECT tax_rate FROM orders WHERE id = ?");
            $stmt_order_tax->execute([$payment['order_id']]);
            $order_tax_info = $stmt_order_tax->fetch();

            if ($order_tax_info && $order_tax_info['tax_rate'] > 0) {
                $tax_rate = $order_tax_info['tax_rate'] / 100;
                $tax_inclusive_price = $base_unit_price * (1 + $tax_rate);

            }

            // Map billing cycles
            $interval_map = ['monthly' => 'month', 'quarterly' => 'month', 'semi_annually' => 'month', 'annually' => 'year'];
            $interval_count_map = ['monthly' => 1, 'quarterly' => 3, 'semi_annually' => 6, 'annually' => 1];

            $interval = $interval_map[$db_sub['billing_cycle']] ?? 'month';
            $interval_count = $interval_count_map[$db_sub['billing_cycle']] ?? 1;

            // Create price
            $price_data = [
                'unit_amount' => (int) round($tax_inclusive_price * 100),
                'currency' => strtolower($payment['currency']),
                'recurring[interval]' => $interval,
                'recurring[interval_count]' => $interval_count,
                'product_data[name]' => $db_sub['product_name'],
                'product_data[metadata][product_id]' => $db_sub['product_id'],
                'product_data[metadata][order_id]' => $payment['order_id']
            ];

            $price = $makeStripeAPICall('prices', $secret_key, $price_data);

            // Create subscription
            $subscription_data = [
                'customer' => $customer['id'],
                'items[0][price]' => $price['id'],
                'items[0][quantity]' => $db_sub['quantity'],
                'payment_behavior' => 'default_incomplete',
                'payment_settings[save_default_payment_method]' => 'on_subscription',
                'expand[]' => 'latest_invoice.payment_intent',
                'metadata[payment_id]' => $payment_id,
                'metadata[order_id]' => $payment['order_id'],
                'metadata[order_number]' => $payment['order_number'],
                'metadata[product_id]' => $db_sub['product_id'],
                'metadata[product_name]' => $db_sub['product_name']
            ];

            if ($db_sub['trial_days'] && $db_sub['trial_days'] > 0) {
                $subscription_data['trial_period_days'] = $db_sub['trial_days'];
            }

            $stripe_subscription = $makeStripeAPICall('subscriptions', $secret_key, $subscription_data);

            // Confirm first invoice payment
            if (isset($stripe_subscription['latest_invoice']['payment_intent'])) {
                $payment_intent_id = $stripe_subscription['latest_invoice']['payment_intent']['id'] ?? null;
                if ($payment_intent_id && $stripe_subscription['latest_invoice']['status'] === 'open') {
                    $confirm_data = [
                        'payment_method' => $payment_method_id,
                        'return_url' => getCleanUrl('order_confirmation/' . $payment['order_id'])
                    ];
                    $makeStripeAPICall("payment_intents/$payment_intent_id/confirm", $secret_key, $confirm_data);
                }
            }

            // Update subscription record with Stripe data
            $gateway_metadata = json_encode([
                'stripe_customer_id' => $customer['id'],
                'stripe_subscription_id' => $stripe_subscription['id'],
                'stripe_price_id' => $price['id'],
                'stripe_product_id' => $price['product'],
                'payment_method_id' => $payment_method_id
            ]);

            $start_date = date('Y-m-d');
            $subscription_status = ($db_sub['trial_days'] && $db_sub['trial_days'] > 0) ? 'trial' : 'active';
            $next_billing_date = $db_sub['trial_days'] && $db_sub['trial_days'] > 0
                ? date('Y-m-d', strtotime("+{$db_sub['trial_days']} days"))
                : date('Y-m-d', strtotime("+{$interval_count} {$interval}"));

            $stmt = $pdo->prepare("
                UPDATE subscriptions 
                SET gateway_subscription_id = ?, status = ?, start_date = ?, next_billing_date = ?, 
                    trial_end_date = ?, gateway_metadata = ?
                WHERE id = ?
            ");

            $stmt->execute([
                $stripe_subscription['id'],
                $subscription_status,
                $start_date,
                $next_billing_date,
                $db_sub['trial_days'] && $db_sub['trial_days'] > 0 ? $next_billing_date : null,
                $gateway_metadata,
                $db_sub['id']
            ]);

            $created_subscriptions[] = [
                'subscription_id' => $db_sub['id'],
                'stripe_subscription_id' => $stripe_subscription['id'],
                'product_name' => $db_sub['product_name']
            ];
        }

        // Update payment status
        $stmt = $pdo->prepare("
            UPDATE payments 
            SET status = 'succeeded', processed_at = NOW(), 
                gateway_response = ?, payment_method = 'card'
            WHERE id = ?
        ");

        $gateway_response = json_encode([
            'subscriptions_created' => count($created_subscriptions),
            'stripe_customer_id' => $customer['id'],
            'subscriptions' => array_column($created_subscriptions, 'stripe_subscription_id')
        ]);

        $stmt->execute([$gateway_response, $payment_id]);

        // Commit transaction
        $pdo->commit();

        // Restore original isolation level
        if (isset($original_isolation)) {
            try {
                $pdo->exec("SET SESSION transaction_isolation = '" . addslashes($original_isolation) . "'");
            } catch (Exception $restore_error) {
                error_log("Failed to restore isolation level: " . sanitizeLogMessage($restore_error->getMessage()));
            }
        }
        $pdo->exec("SET autocommit = 1");

        return [
            'success' => true,
            'order_id' => $payment['order_id'],
            'order_number' => $payment['order_number'],
            'subscriptions' => $created_subscriptions,
            'customer_id' => $customer['id'],
            'redirect_url' => getCleanUrl('order_confirmation/' . $payment['order_id']),
            'message' => 'Subscription created successfully'
        ];

    } catch (Exception $e) {
        if ($pdo->inTransaction()) {
            $pdo->rollBack();
        }
        // Restore original isolation level on error
        if (isset($original_isolation)) {
            try {
                $pdo->exec("SET SESSION transaction_isolation = '" . addslashes($original_isolation) . "'");
            } catch (Exception $restore_error) {
                error_log("Failed to restore isolation level: " . sanitizeLogMessage($restore_error->getMessage()));
            }
        }
        $pdo->exec("SET autocommit = 1");


        return ['success' => false, 'error' => $e->getMessage()];
    }
}

/**
 * Helper function to safely load language files
 * 
 * @param string $filename The language file name (e.g., 'lang_frontend.php')
 * @param string $language_override Optional language override (defaults to SITE_LANGUAGE)
 * @return array The language array or empty array if file doesn't exist
 */
function loadLanguageFile($filename, $language_override = null)
{
    // Use override language or fall back to SITE_LANGUAGE constant
    $language = $language_override ?? (defined('SITE_LANGUAGE') ? SITE_LANGUAGE : 'english-us');

    // For backward compatibility, we now point to the consolidated lang.php instead of fragmented files
    $language_path = __DIR__ . '/../languages/' . $language . '/lang.php';

    // Return loaded array or empty array if file doesn't exist
    return file_exists($language_path) ? include($language_path) : [];
}

/**
 * Helper function to load all common language files at once
 * 
 * @param string $language_override Optional language override
 * @return array Associative array with all language files loaded
 */
function loadAllLanguageFiles($language_override = null)
{
    $lang_data = loadLanguageFile('lang.php', $language_override);
    return [
        'frontend' => $lang_data,
        'alerts' => $lang_data,
        'navigation' => $lang_data,
        'email' => $lang_data,
        'admin' => $lang_data,
        'config' => $lang_data
    ];
}

/**
 * Validate session for AJAX endpoints
 * Returns true if session is valid, sends error response and returns false if invalid
 */
function validateAjaxSession()
{
    // Ensure session is initialized
    if (!SessionManager::isInitialized()) {
        SessionManager::initialize();
    }
    if (!SessionManager::validateSession()) {
        header('Content-Type: application/json');
        http_response_code(401);
        echo json_encode([
            'success' => false,
            'error' => 'Session expired or invalid',
            'redirect' => getCleanUrl('login')
        ]);
        return false;
    }
    return true;
}

/**
 * Get available languages from the languages directory
 * Returns array of language codes and names
 */
function getAvailableLanguages()
{
    $languages = [];
    $langDir = __DIR__ . '/../languages/';
    $dirs = array_filter(glob($langDir . '*'), 'is_dir');

    foreach ($dirs as $dir) {
        $langCode = basename($dir);
        // Get first part before hyphen (e.g., "English" from "English-US")
        $langName = explode('-', $langCode)[0];
        $languages[$langCode] = $langName;
    }

    return $languages;
}

/**
 * Returns the active currency code and symbol from Stripe gateway settings, or defaults to USD/$.
 * @return array ['code' => 'USD', 'symbol' => '$']
 */
function get_active_currency()
{
    global $pdo;
    static $currency_cache = null;
    if ($currency_cache !== null)
        return $currency_cache;
    $currency_code = 'USD';
    $currency_symbol = '$';
    try {
        $stmt = $pdo->prepare("SELECT settings FROM payment_gateways WHERE gateway_key = 'stripe' AND is_enabled = 1 LIMIT 1");
        $stmt->execute();
        $gateway = $stmt->fetch();
        if ($gateway && !empty($gateway['settings'])) {
            $settings = json_decode($gateway['settings'], true);
            if (!empty($settings['currency'])) {
                $currency_code = strtoupper($settings['currency']);
            }
        }
    } catch (Exception $e) {
    }
    // Map common currency codes to symbols
    $symbols = [
        'USD' => '$',
        'EUR' => '€',
        'GBP' => '£',
        'CAD' => 'C$',
        'AUD' => 'A$',
    ];
    if (isset($symbols[$currency_code])) {
        $currency_symbol = $symbols[$currency_code];
    } else {
        $currency_symbol = $currency_code . ' ';
    }
    $currency_cache = ['code' => $currency_code, 'symbol' => $currency_symbol];
    return $currency_cache;
}

/**
 * Returns only the active currency symbol (e.g., $, €, £, etc.)
 */
function get_active_currency_symbol()
{
    $cur = get_active_currency();
    return $cur['symbol'];
}

function api_authenticate()
{
    $headers = function_exists('getallheaders') ? getallheaders() : [];
    $token = '';

    // Security: Only accept tokens from headers or POST body, never from GET parameters
    // GET parameters can appear in server logs, access logs, and browser history
    if (isset($headers['Authorization'])) {
        $token = trim(str_replace('Bearer', '', $headers['Authorization']));
    } elseif (isset($_POST['api_token'])) {
        $token = $_POST['api_token'];
    }

    // Log security warning if token was attempted via GET
    if (isset($_GET['api_token'])) {
        error_log("Security Warning: API token attempted via GET parameter from IP: " . ($_SERVER['REMOTE_ADDR'] ?? 'unknown'));
        // Do not use the GET parameter - security risk
    }

    // Use constant-time comparison to prevent timing attacks
    if (empty($token) || !hash_equals(API_TOKEN, $token)) {
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }
}

/**
 * Returns a human-friendly 'time ago' string (e.g., '2 minutes ago', '1 week ago').
 * @param string|DateTime $datetime The date/time to compare to now
 * @param array $lang Optional language array for translation
 * @return string
 */
function timeAgo($datetime, $lang = [])
{
    if (empty($datetime) || $datetime === '0000-00-00 00:00:00' || $datetime === '0000-00-00') {
        return '';
    }
    try {
        if (!$datetime instanceof DateTimeInterface) {
            $date = new DateTime($datetime, new DateTimeZone('UTC'));
        } else {
            $date = $datetime;
        }
        $now = new DateTime('now', new DateTimeZone('UTC'));
        $diff = $now->diff($date);
        $seconds = $now->getTimestamp() - $date->getTimestamp();
        if ($seconds < 0)
            return '';

        $langDefaults = [
            'just_now' => 'just now',
            'minute_ago' => 'one minute ago',
            'minutes_ago' => '%d minutes ago',
            'hour_ago' => 'one hour ago',
            'hours_ago' => '%d hours ago',
            'day_ago' => 'yesterday',
            'days_ago' => '%d days ago',
            'week_ago' => 'one week ago',
            'weeks_ago' => '%d weeks ago',
            'month_ago' => 'one month ago',
            'months_ago' => '%d months ago',
            'year_ago' => 'one year ago',
            'years_ago' => '%d years ago',
        ];
        $l = array_merge($langDefaults, $lang);
        if ($seconds < 60) {
            return $l['just_now'];
        } elseif ($seconds < 120) {
            return $l['minute_ago'];
        } elseif ($seconds < 3600) {
            return sprintf($l['minutes_ago'], floor($seconds / 60));
        } elseif ($seconds < 7200) {
            return $l['hour_ago'];
        } elseif ($seconds < 86400) {
            return sprintf($l['hours_ago'], floor($seconds / 3600));
        } elseif ($seconds < 172800) {
            return $l['day_ago'];
        } elseif ($seconds < 604800) {
            return sprintf($l['days_ago'], floor($seconds / 86400));
        } elseif ($seconds < 1209600) {
            return $l['week_ago'];
        } elseif ($seconds < 2592000) {
            return sprintf($l['weeks_ago'], floor($seconds / 604800));
        } elseif ($seconds < 5184000) {
            return $l['month_ago'];
        } elseif ($seconds < 31536000) {
            return sprintf($l['months_ago'], floor($seconds / 2592000));
        } elseif ($seconds < 63072000) {
            return $l['year_ago'];
        } else {
            return sprintf($l['years_ago'], floor($seconds / 31536000));
        }
    } catch (Exception $e) {
        return '';
    }
}
/**
 * Check IP geolocation and block banned countries using IPinfo.io API
 * Redirects to configured URL if country is banned
 * 
 * @return void Exits and redirects if country is banned
 */
function checkCountryBlocking()
{
    // Skip check if IPinfo is not configured
    if (!defined('IPINFO_API_KEY') || empty(IPINFO_API_KEY)) {
        return;
    }

    // Skip check if no banned countries configured
    if (!defined('IPINFO_BANNED_COUNTRIES') || empty(IPINFO_BANNED_COUNTRIES)) {
        return;
    }

    // Skip check for install.php to allow installation from any location
    // Skip check for webhook endpoints (they come from external services, not end users)
    $current_script = basename($_SERVER['SCRIPT_NAME'] ?? '');
    $request_uri = $_SERVER['REQUEST_URI'] ?? '';

    if (
        $current_script === 'install.php' ||
        strpos($request_uri, '/webhook/') !== false ||
        strpos($request_uri, 'webhook') !== false
    ) {
        return;
    }

    // Get client IP address
    $ip_address = $_SERVER['REMOTE_ADDR'] ?? '';

    // Skip if IP is localhost or private IP
    if (
        empty($ip_address) ||
        $ip_address === '127.0.0.1' ||
        $ip_address === '::1' ||
        filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
    ) {
        return;
    }

    // Parse banned countries list
    $banned_countries = array_map('trim', array_map('strtoupper', explode(',', IPINFO_BANNED_COUNTRIES)));
    $banned_countries = array_filter($banned_countries); // Remove empty values

    if (empty($banned_countries)) {
        return;
    }

    try {
        // Call IPinfo.io API
        $api_url = 'https://ipinfo.io/' . urlencode($ip_address) . '/country?token=' . urlencode(IPINFO_API_KEY);

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $api_url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 3); // 3 second timeout
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 2); // 2 second connection timeout
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);

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

        // If API call fails, allow access (fail open to prevent blocking legitimate users)
        if ($response === false || $http_code !== 200 || !empty($curl_error)) {
            error_log("IPinfo API call failed for IP {$ip_address}: HTTP {$http_code}, Error: {$curl_error}");
            return; // Fail open - don't block if API is unavailable
        }

        // IPinfo /country endpoint returns country code as plain text (e.g., "US" or "CN")
        // Validate response is a valid 2-letter country code
        $country_code = trim(strtoupper($response));

        // Validate country code format (2 uppercase letters)
        if (empty($country_code) || strlen($country_code) !== 2 || !preg_match('/^[A-Z]{2}$/', $country_code)) {
            error_log("IPinfo returned invalid country code format: " . substr($response, 0, 50));
            return; // Fail open - don't block if response is invalid
        }

        // Check if country is banned
        if (in_array($country_code, $banned_countries)) {
            // Log the blocking attempt
            error_log("Country blocking: IP {$ip_address} from banned country {$country_code} - redirecting");

            // Get redirect URL
            $redirect_url = defined('IPINFO_BLOCK_REDIRECT_URL') && !empty(IPINFO_BLOCK_REDIRECT_URL)
                ? IPINFO_BLOCK_REDIRECT_URL
                : 'https://www.google.com';

            // Clean output buffers before redirect
            while (ob_get_level() > 0) {
                ob_end_clean();
            }

            // Redirect to configured URL
            header("Location: " . $redirect_url, true, 302);
            exit;
        }

    } catch (Exception $e) {
        // Log error but fail open (don't block legitimate users if API fails)
        error_log("Country blocking check error: " . $e->getMessage());
        return;
    }
}

/**
 * Sanitizes an HTML string based on a whitelist of tags and attributes.
 * This is used for rich text content (e.g. from Summernote) to prevent XSS.
 * 
 * @param string $html The HTML string to sanitize.
 * @return string The sanitized HTML string.
 */
function cleanHTML($html)
{
    if (empty($html)) {
        return '';
    }

    // Initialize DOMDocument
    $dom = new DOMDocument();

    // Suppress errors for invalid HTML
    libxml_use_internal_errors(true);

    // Pre-process to escape stray < characters that are not part of a valid-looking tag
    // This prevents DOMDocument from truncating content if it encounters a raw <
    $html = preg_replace('/<(?![a-zA-Z\/!])/', '&lt;', $html);

    // Load the HTML with UTF-8 encoding
    // Handle the issue where DOMDocument adds body/html tags or wraps in unintended elements
    $dom->loadHTML('<?xml encoding="UTF-8">' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);

    libxml_clear_errors();

    // Whitelist definitions
    $allowedTags = ['p', 'b', 'i', 'u', 'ul', 'ol', 'li', 'br', 'a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'span', 'img', 'blockquote', 'code', 'pre', 'strong', 'em', 'table', 'thead', 'tbody', 'tr', 'th', 'td'];
    $allowedAttributes = [
        'href',
        'src',
        'alt',
        'title',
        'class',
        'id',
        'width',
        'height',
        'style',
        'target',
        'rel'
    ];
    $forbiddenStyles = ['expression', 'javascript', 'behavior', '-moz-binding', 'vbscript'];

    // Traverse all nodes
    $nodes = $dom->getElementsByTagName('*');
    for ($i = $nodes->length - 1; $i >= 0; $i--) {
        $node = $nodes->item($i);

        // Check tag name
        if (!in_array(strtolower($node->tagName), $allowedTags)) {
            // Remove tag but keep contents if it is a tag that should not be removed entirely
            // This is complex, for simplicity we just remove the node and its children if it is forbidden
            // But we don't want to remove everything if someone just types <script>
            // Actually, for security, removing the node and its children is safer.
            $node->parentNode->removeChild($node);
            continue;
        }

        // Clean attributes
        if ($node->hasAttributes()) {
            $attributes = [];
            foreach ($node->attributes as $attr) {
                $attributes[] = $attr->nodeName;
            }

            foreach ($attributes as $attrName) {
                $attrLower = strtolower($attrName);

                // Remove forbidden attributes
                if (!in_array($attrLower, $allowedAttributes) || strpos($attrLower, 'on') === 0) {
                    $node->removeAttribute($attrName);
                    continue;
                }

                // Sanitize attribute values (e.g. href="javascript:...")
                $attrValue = $node->getAttribute($attrName);
                if (
                    ($attrLower === 'href' || $attrLower === 'src' || $attrLower === 'action') &&
                    preg_match('/^\s*(javascript|vbscript|data):/i', $attrValue)
                ) {
                    $node->removeAttribute($attrName);
                    continue;
                }

                // Sanitize style attribute
                if ($attrLower === 'style') {
                    foreach ($forbiddenStyles as $forbidden) {
                        if (stripos($attrValue, $forbidden) !== false) {
                            $node->removeAttribute($attrName);
                            break;
                        }
                    }
                }
            }
        }
    }

    // Save and return the sanitized HTML
    $output = $dom->saveHTML();
    // Clean up DOMDocument artifacts
    $output = str_replace('<?xml encoding="UTF-8">', '', $output);
    return trim($output);
}

/**
 * Sanitize a message for logging by removing sensitive information and normalizing whitespace.
 */
function sanitizeLogMessage($message)
{
    if (!is_string($message))
        return (string) $message;
    $message = preg_replace('/(password|secret|token|api_key)\s*=\s*[^\s&;]+/i', '$1=***', $message);
    return trim(str_replace(["\r", "\n", "\t"], ' ', $message));
}

/**
 * Validate the content of a file against its purported extension.
 */
function validateFileContent($filePath, $extension)
{
    if (!file_exists($filePath)) {
        return ['valid' => false, 'reason' => 'File does not exist'];
    }

    $extension = strtolower(ltrim($extension, '.'));

    // Initialize fileinfo
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    if (!$finfo) {
        return ['valid' => true, 'reason' => 'Fileinfo extension not available, skipped check'];
    }

    $mimeType = finfo_file($finfo, $filePath);
    finfo_close($finfo);

    $mimeMap = [
        'jpg' => ['image/jpeg', 'image/pjpeg'],
        'jpeg' => ['image/jpeg', 'image/pjpeg'],
        'png' => ['image/png', 'image/x-png'],
        'gif' => ['image/gif'],
        'webp' => ['image/webp'],
        'svg' => ['image/svg+xml'],
        'pdf' => ['application/pdf'],
        'txt' => ['text/plain'],
        'csv' => ['text/csv', 'text/plain'],
        'doc' => ['application/msword'],
        'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
        'xls' => ['application/vnd.ms-excel'],
        'xlsx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
        'zip' => ['application/zip', 'application/x-zip-compressed', 'application/x-zip', 'application/octet-stream']
    ];

    if (isset($mimeMap[$extension])) {
        $isValid = in_array($mimeType, $mimeMap[$extension]);
        return [
            'valid' => $isValid,
            'reason' => $isValid ? '' : "MIME type mismatch: expected one of " . implode(', ', $mimeMap[$extension]) . " but got $mimeType"
        ];
    }

    return [
        'valid' => true,
        'reason' => 'Extension not in strict check list'
    ];
}

/**
 * Triggers external webhooks configured for products in an order.
 * Ensures that third-party systems are notified when a purchase is successful.
 *
 * @param int $order_id The ID of the order to trigger webhooks for.
 * @return void
 */
function trigger_product_webhooks($order_id)
{
    global $pdo;

    try {
        // 1. Fetch order details
        $stmt_order = $pdo->prepare("SELECT * FROM orders WHERE id = ?");
        $stmt_order->execute([$order_id]);
        $order = $stmt_order->fetch();

        if (!$order) {
            return;
        }

        // 2. Fetch order items
        $stmt_items = $pdo->prepare("SELECT * FROM order_items WHERE order_id = ?");
        $stmt_items->execute([$order_id]);
        $items = $stmt_items->fetchAll();

        if (empty($items)) {
            return;
        }

        // 3. Get currency symbol
        $currency_info = get_active_currency();
        $currency_symbol = $currency_info['symbol'];

        // 4. Iterate through items and trigger webhooks
        foreach ($items as $item) {
            $webhook_stmt = $pdo->prepare("SELECT * FROM webhooks WHERE product_id = ? AND is_enabled = 1");
            $webhook_stmt->execute([$item['product_id']]);
            $webhooks = $webhook_stmt->fetchAll();

            if ($webhooks) {
                $payload = [
                    'order_id' => $order['id'],
                    'order_number' => $order['order_number'],
                    'product_id' => $item['product_id'],
                    'product_name' => $item['product_name'],
                    'quantity' => $item['quantity'],
                    'unit_price' => $item['unit_price'],
                    'total_price' => $item['total_price'],
                    'client_name' => $order['client_name'],
                    'client_email' => $order['client_email'],
                    'billing_address' => $order['billing_address'],
                    'subtotal' => $order['subtotal'],
                    'tax_rate' => $order['tax_rate'],
                    'tax_amount' => $order['tax_amount'],
                    'total_amount' => $order['total_amount'],
                    'currency' => $currency_symbol,
                    'timestamp' => date('Y-m-d H:i:s')
                ];

                foreach ($webhooks as $wh) {
                    $url = $wh['url'];
                    $method = strtolower($wh['method'] ?? 'post');

                    $ch = curl_init();
                    if ($method === 'get') {
                        $query = http_build_query($payload);
                        $webhook_url = strpos($url, '?') === false ? "$url?$query" : "$url&$query";
                        curl_setopt($ch, CURLOPT_URL, $webhook_url);
                        curl_setopt($ch, CURLOPT_HTTPGET, true);
                    } else {
                        curl_setopt($ch, CURLOPT_URL, $url);
                        curl_setopt($ch, CURLOPT_POST, true);
                        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
                    }

                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                    curl_setopt($ch, CURLOPT_TIMEOUT_MS, 500); // Slight increase from 200ms to be safer
                    curl_setopt($ch, CURLOPT_NOSIGNAL, true);
                    curl_exec($ch);
                    curl_close($ch);
                }
            }
        }
    } catch (Throwable $e) {
        error_log("Webhook Trigger Error for Order $order_id: " . $e->getMessage());
    }
}
