<?php
// ==========================================================
// CORE CONFIG
// ==========================================================
date_default_timezone_set('Asia/Kolkata');

define('DATA_DIR', __DIR__ . '/../data');
define('USERS_FILE', DATA_DIR . '/users.json');
define('PLANS_FILE', DATA_DIR . '/plans.json');
define('ADMIN_SETTINGS_FILE', DATA_DIR . '/admin_settings.json');
define('ORDERS_FILE', DATA_DIR . '/orders.json');

// Secret key used to encrypt Gmail App Passwords before storing in JSON.
// IMPORTANT: Change this to your own random string when deploying.
define('ENCRYPTION_KEY', 'ChangeThisToYourOwnSecretKey_9273');

// Admin hardcoded login (owner only - not stored in users.json)
define('ADMIN_USERNAME', 'Aditya268');
define('ADMIN_PASSWORD', 'Aditya@2627');

// ==========================================================
// DATABASE CONFIG (MariaDB/MySQL)
// ------------------------------------------------------------
// The app previously stored everything in data/*.json files.
// It now persists everything in the MySQL/MariaDB database below.
// readJson()/writeJson() below are drop-in replacements that map each
// legacy "file" constant to a table, so the rest of the app needed no
// changes. The original JSON files are kept as an untouched backup in
// data/ and are no longer read or written by the application.
// ==========================================================
define('DB_HOST', 'localhost');
define('DB_NAME', 'narutomy_fampay');
define('DB_USER', 'narutomy_fampay');
define('DB_PASS', '0866de99ac703727e5791b33Aa@1');

function dbConnection() {
    static $pdo = null;
    if ($pdo === null) {
        $pdo = new PDO(
            'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4',
            DB_USER,
            DB_PASS,
            [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES => false
            ]
        );
    }
    return $pdo;
}

/**
 * Returns the table name that maps to a legacy data/*.json constant,
 * or null if the given path is not one of the known storage files.
 */
function tableForFile($file) {
    switch ($file) {
        case USERS_FILE: return 'users';
        case ORDERS_FILE: return 'orders';
        case PLANS_FILE: return 'plans';
        case ADMIN_SETTINGS_FILE: return 'admin_settings';
        case SHORTLINKS_FILE: return 'shortlinks';
    }
    return null;
}

/**
 * Converts a database value back to its original PHP number type
 * (int when whole, float otherwise) so reconstructed arrays match the
 * shapes the rest of the app expects.
 */
function dbNum($value) {
    $value = (float)$value;
    return $value == (int)$value ? (int)$value : $value;
}

function dbUserToArray($row) {
    $apiKeys = json_decode($row['api_keys'] ?? '', true);
    $user = [
        'id' => $row['id'],
        'username' => $row['username'] ?? '',
        'name' => $row['name'],
        'email' => $row['email'],
        'password_hash' => $row['password_hash'],
        'created_at' => $row['created_at'],
        'fampay_gmail' => $row['fampay_gmail'],
        'fampay_app_password_encrypted' => $row['fampay_app_password_encrypted'],
        'fampay_upi_id' => $row['fampay_upi_id'],
        'gmail_connected' => (bool)$row['gmail_connected'],
        'api_key' => $row['api_key'],
        'plan_id' => $row['plan_id'],
        'plan_label' => $row['plan_label'],
        'plan_expires_at' => $row['plan_expires_at'],
        'total_requests' => (int)$row['total_requests'],
        'total_success' => (int)$row['total_success'],
        'total_failed' => (int)$row['total_failed'],
        'total_amount_received' => dbNum($row['total_amount_received']),
        'status' => $row['status'],
        'api_keys' => is_array($apiKeys) ? $apiKeys : [],
        'reset_chances' => (int)$row['reset_chances'],
        'avatar' => $row['avatar'],
        'webhook_url' => $row['webhook_url'] ?? '',
        'qr_center_image' => (bool)($row['qr_center_image'] ?? 0),
        'email_verified' => (bool)($row['email_verified'] ?? 1),
    ];
    return $user;
}

function dbOrderToArray($row) {
    $order = [
        'order_id' => $row['order_id'],
        'type' => $row['type'],
        'user_id' => $row['user_id'],
        'plan_id' => $row['plan_id'],
        'amount' => dbNum($row['amount']),
        'status' => $row['status'],
        'created_at' => $row['created_at'],
        'created_at_timestamp' => (int)$row['created_at_timestamp'],
    ];
    if ($row['transaction_id'] !== '') $order['transaction_id'] = $row['transaction_id'];
    if ($row['utr'] !== '') $order['utr'] = $row['utr'];
    if ($row['confirmed_at'] !== '') $order['confirmed_at'] = $row['confirmed_at'];
    if ($row['upi_id'] !== '') $order['upi_id'] = $row['upi_id'];
    if ((int)$row['expires_at_timestamp'] > 0) $order['expires_at_timestamp'] = (int)$row['expires_at_timestamp'];
    if ($row['sender_name'] !== '') $order['sender_name'] = $row['sender_name'];
    if ($row['payment_time_ist'] !== '') $order['payment_time_ist'] = $row['payment_time_ist'];
    if ($row['note_code'] !== '') $order['note_code'] = $row['note_code'];
    if ($row['pay_link_id'] !== '') $order['pay_link_id'] = $row['pay_link_id'];
    return $order;
}

function dbPlanToArray($row) {
    return [
        'id' => $row['id'],
        'price' => dbNum($row['price']),
        'days' => (int)$row['days'],
        'original_price' => $row['original_price'] === null ? null : dbNum($row['original_price']),
        'offer' => (bool)$row['offer'],
        'label' => $row['label'],
    ];
}

function dbShortlinkToArray($row) {
    $link = [
        'code' => $row['code'],
        'target_url' => $row['target_url'],
        'owner_user_id' => $row['owner_user_id'],
        'created_at' => $row['created_at'],
        'clicks' => (int)$row['clicks'],
    ];
    if ((int)$row['created_at_timestamp'] > 0) $link['created_at_timestamp'] = (int)$row['created_at_timestamp'];
    if ((int)$row['expires_at_timestamp'] > 0) $link['expires_at_timestamp'] = (int)$row['expires_at_timestamp'];
    return $link;
}

// ==========================================================
// DATABASE-BACKED READ / WRITE HELPERS (drop-in for the old JSON files)
// ------------------------------------------------------------
// readJson()/writeJson() keep the exact same names and return the same
// PHP array shapes as the old file-based versions, so every caller in
// the app (including admin/*.php and the api/*.php endpoints) keeps
// working unchanged. Rows are reconstructed into the original key order
// and only present fields are included, mirroring the old JSON layout.
// ==========================================================
function readJson($file) {
    $table = tableForFile($file);
    if ($table === null) {
        // Not one of our tables - fall back to the original file if present.
        if (!file_exists($file)) return [];
        $content = @file_get_contents($file);
        $data = json_decode($content, true);
        return is_array($data) ? $data : [];
    }

    $pdo = dbConnection();

    if ($table === 'users') {
        $rows = $pdo->query('SELECT * FROM users')->fetchAll();
        $users = [];
        foreach ($rows as $row) $users[] = dbUserToArray($row);
        return $users;
    }

    if ($table === 'orders') {
        $rows = $pdo->query('SELECT * FROM orders')->fetchAll();
        $orders = [];
        foreach ($rows as $row) $orders[] = dbOrderToArray($row);
        return $orders;
    }

    if ($table === 'plans') {
        $rows = $pdo->query('SELECT * FROM plans ORDER BY price ASC')->fetchAll();
        $plans = [];
        foreach ($rows as $row) $plans[] = dbPlanToArray($row);
        return $plans;
    }

    if ($table === 'admin_settings') {
        $row = $pdo->query('SELECT * FROM admin_settings WHERE id = 1')->fetch();
        if (!$row) return [];
        return [
            'gmail_id' => $row['gmail_id'],
            'app_password_encrypted' => $row['app_password_encrypted'],
            'upi_id' => $row['upi_id'],
            'custom_api_price' => dbNum($row['custom_api_price']),
            'reset_unlock_price' => dbNum($row['reset_unlock_price']),
            'base_url' => $row['base_url'],
            'demo_amount' => dbNum($row['demo_amount']),
        ];
    }

    if ($table === 'shortlinks') {
        $rows = $pdo->query('SELECT * FROM shortlinks')->fetchAll();
        $links = [];
        foreach ($rows as $row) {
            $link = dbShortlinkToArray($row);
            $links[$link['code']] = $link;
        }
        return $links;
    }

    return [];
}

function writeJson($file, $data) {
    $table = tableForFile($file);
    if ($table === null) {
        // Not one of our tables - fall back to the original file if present.
        $fp = fopen($file, 'c');
        if (!$fp) return false;
        flock($fp, LOCK_EX);
        ftruncate($fp, 0);
        rewind($fp);
        fwrite($fp, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
        fflush($fp);
        flock($fp, LOCK_UN);
        fclose($fp);
        return true;
    }

    $pdo = dbConnection();
    try {
        $pdo->beginTransaction();

        if ($table === 'admin_settings') {
            $stmt = $pdo->prepare(
                'INSERT INTO admin_settings (id, gmail_id, app_password_encrypted, upi_id, custom_api_price, reset_unlock_price, base_url, demo_amount)
                 VALUES (1, ?, ?, ?, ?, ?, ?, ?)
                 ON DUPLICATE KEY UPDATE
                   gmail_id = VALUES(gmail_id),
                   app_password_encrypted = VALUES(app_password_encrypted),
                   upi_id = VALUES(upi_id),
                   custom_api_price = VALUES(custom_api_price),
                   reset_unlock_price = VALUES(reset_unlock_price),
                   base_url = VALUES(base_url),
                   demo_amount = VALUES(demo_amount)'
            );
            $stmt->execute([
                $data['gmail_id'] ?? '',
                $data['app_password_encrypted'] ?? '',
                $data['upi_id'] ?? '',
                $data['custom_api_price'] ?? 100,
                $data['reset_unlock_price'] ?? 5,
                $data['base_url'] ?? '',
                $data['demo_amount'] ?? 1,
            ]);
            $pdo->commit();
            return true;
        }

        if ($table === 'plans') {
            $pdo->exec('DELETE FROM plans');
            $stmt = $pdo->prepare(
                'INSERT INTO plans (id, price, days, original_price, offer, label) VALUES (?, ?, ?, ?, ?, ?)'
            );
            foreach ($data as $plan) {
                $stmt->execute([
                    $plan['id'] ?? '',
                    $plan['price'] ?? 0,
                    $plan['days'] ?? 0,
                    isset($plan['original_price']) && $plan['original_price'] !== null ? $plan['original_price'] : null,
                    !empty($plan['offer']) ? 1 : 0,
                    $plan['label'] ?? '',
                ]);
            }
            $pdo->commit();
            return true;
        }

        if ($table === 'users') {
            $pdo->exec('DELETE FROM users');
            $stmt = $pdo->prepare(
                'INSERT INTO users (id, username, name, email, password_hash, created_at, fampay_gmail, fampay_app_password_encrypted, fampay_upi_id, gmail_connected, api_key, plan_id, plan_label, plan_expires_at, total_requests, total_success, total_failed, total_amount_received, status, api_keys, reset_chances, avatar, webhook_url, qr_center_image, email_verified)
                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
            );
            foreach ($data as $user) {
                $stmt->execute([
                    $user['id'] ?? '',
                    $user['username'] ?? '',
                    $user['name'] ?? '',
                    $user['email'] ?? '',
                    $user['password_hash'] ?? '',
                    $user['created_at'] ?? '',
                    $user['fampay_gmail'] ?? '',
                    $user['fampay_app_password_encrypted'] ?? '',
                    $user['fampay_upi_id'] ?? '',
                    !empty($user['gmail_connected']) ? 1 : 0,
                    $user['api_key'] ?? '',
                    $user['plan_id'] ?? '',
                    $user['plan_label'] ?? '',
                    $user['plan_expires_at'] ?? '',
                    $user['total_requests'] ?? 0,
                    $user['total_success'] ?? 0,
                    $user['total_failed'] ?? 0,
                    $user['total_amount_received'] ?? 0,
                    $user['status'] ?? 'active',
                    json_encode($user['api_keys'] ?? [], JSON_UNESCAPED_UNICODE),
                    $user['reset_chances'] ?? 10,
                    $user['avatar'] ?? '',
                    $user['webhook_url'] ?? '',
                    !empty($user['qr_center_image']) ? 1 : 0,
                    !empty($user['email_verified']) ? 1 : 1,
                ]);
            }
            $pdo->commit();
            return true;
        }

        if ($table === 'shortlinks') {
            $pdo->exec('DELETE FROM shortlinks');
            $stmt = $pdo->prepare(
                'INSERT INTO shortlinks (code, target_url, owner_user_id, created_at, created_at_timestamp, expires_at_timestamp, clicks)
                 VALUES (?, ?, ?, ?, ?, ?, ?)'
            );
            foreach ($data as $code => $link) {
                $stmt->execute([
                    $link['code'] ?? $code,
                    $link['target_url'] ?? '',
                    $link['owner_user_id'] ?? '',
                    $link['created_at'] ?? '',
                    $link['created_at_timestamp'] ?? 0,
                    $link['expires_at_timestamp'] ?? 0,
                    $link['clicks'] ?? 0,
                ]);
            }
            $pdo->commit();
            return true;
        }

        if ($table === 'orders') {
            $pdo->exec('DELETE FROM orders');
            $stmt = $pdo->prepare(
                'INSERT INTO orders (order_id, type, user_id, plan_id, upi_id, amount, status, created_at, created_at_timestamp, expires_at_timestamp, transaction_id, utr, confirmed_at, sender_name, payment_time_ist, note_code, pay_link_id)
                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
            );
            foreach ($data as $order) {
                $stmt->execute([
                    $order['order_id'] ?? '',
                    $order['type'] ?? '',
                    $order['user_id'] ?? '',
                    $order['plan_id'] ?? '',
                    $order['upi_id'] ?? '',
                    $order['amount'] ?? 0,
                    $order['status'] ?? '',
                    $order['created_at'] ?? '',
                    $order['created_at_timestamp'] ?? 0,
                    $order['expires_at_timestamp'] ?? 0,
                    $order['transaction_id'] ?? '',
                    $order['utr'] ?? '',
                    $order['confirmed_at'] ?? '',
                    $order['sender_name'] ?? '',
                    $order['payment_time_ist'] ?? '',
                    $order['note_code'] ?? '',
                    $order['pay_link_id'] ?? '',
                ]);
            }
            $pdo->commit();
            return true;
        }

        $pdo->commit();
    } catch (Exception $e) {
        try { $pdo->rollBack(); } catch (Exception $x) {}
        return false;
    }
    return true;
}

// ==========================================================
// ENCRYPTION HELPERS (for storing Gmail App Passwords safely)
// ==========================================================
function encryptData($plainText) {
    $iv = openssl_random_pseudo_bytes(16);
    $encrypted = openssl_encrypt($plainText, 'AES-256-CBC', ENCRYPTION_KEY, 0, $iv);
    return base64_encode($iv . $encrypted);
}

function decryptData($encryptedText) {
    $data = base64_decode($encryptedText);
    $iv = substr($data, 0, 16);
    $encrypted = substr($data, 16);
    return openssl_decrypt($encrypted, 'AES-256-CBC', ENCRYPTION_KEY, 0, $iv);
}

// ==========================================================
// PAYLOAD ENCRYPTION (for internal dashboard/demo AJAX traffic)
// ------------------------------------------------------------
// Separate key + AES-256-GCM (authenticated encryption, so a tampered
// ciphertext is rejected rather than silently decrypting to garbage).
// This is used to wrap request/response bodies on our OWN internal
// endpoints (the ones only ever called by our own dashboard/demo JS),
// so someone inspecting network traffic in devtools can't read the raw
// order_id/amount/plan_id/etc values or replay-tamper with them.
//
// IMPORTANT: this is NOT used on the public documented API endpoints
// (api/qr.php, api/verify_order.php) — those have a published plain-JSON
// contract that outside developers already integrate against, so
// encrypting them would break every existing integration.
//
// IMPORTANT: Change this to your own freshly-generated key when deploying.
// Generate one with: php -r "echo base64_encode(random_bytes(32));"
define('PAYLOAD_ENCRYPTION_KEY_B64', 'LLaSBLd6+Lao+7OS02Qe/jTfb0th84L33JEhqEUSGrg=');

/**
 * Encrypts a PHP value (array/string/number) into a single opaque,
 * URL-safe base64 token: base64(iv . tag . ciphertext).
 * AES-256-GCM gives both confidentiality AND integrity — if the token
 * is tampered with even by 1 byte, decryptPayload() below returns false
 * instead of trusting corrupted data.
 */
function encryptPayload($value) {
    $json = json_encode($value, JSON_UNESCAPED_UNICODE);
    $iv = openssl_random_pseudo_bytes(12); // 96-bit IV, standard for GCM
    $tag = '';
    $cipherText = openssl_encrypt(
        $json,
        'aes-256-gcm',
        base64_decode(PAYLOAD_ENCRYPTION_KEY_B64),
        OPENSSL_RAW_DATA,
        $iv,
        $tag,
        '',
        16
    );
    if ($cipherText === false) return false;
    return rtrim(strtr(base64_encode($iv . $tag . $cipherText), '+/', '-_'), '=');
}

/**
 * Reverses encryptPayload(). Returns the decoded PHP value, or false if
 * the token is malformed, wrong-key, or has been tampered with.
 */
function decryptPayload($token) {
    if (!is_string($token) || $token === '') return false;
    $raw = base64_decode(strtr($token, '-_', '+/'));
    if ($raw === false || strlen($raw) < 28) return false; // 12 iv + 16 tag minimum

    $iv = substr($raw, 0, 12);
    $tag = substr($raw, 12, 16);
    $cipherText = substr($raw, 28);

    $json = openssl_decrypt(
        $cipherText,
        'aes-256-gcm',
        base64_decode(PAYLOAD_ENCRYPTION_KEY_B64),
        OPENSSL_RAW_DATA,
        $iv,
        $tag
    );
    if ($json === false) return false; // wrong key OR tampered ciphertext/tag

    $decoded = json_decode($json, true);
    return $decoded === null && $json !== 'null' ? false : $decoded;
}

/**
 * Drop-in replacement for jsonResponse() on internal AJAX endpoints:
 * encrypts the whole response body into { "p": "<token>" } instead of
 * returning readable JSON, so devtools/network-tab inspection shows only
 * opaque ciphertext.
 */
function encryptedJsonResponse($data, $httpCode = 200) {
    http_response_code($httpCode);
    header('Content-Type: application/json');
    echo json_encode(['p' => encryptPayload($data)]);
    exit;
}

/**
 * Reads an encrypted request body (sent by our own JS as raw POST body
 * text, e.g. fetch(url, { method: 'POST', body: encryptPayload(...) }))
 * and returns the decoded associative array, or [] if missing/invalid.
 * Internal endpoints should use this instead of reading $_POST directly
 * once their calling JS has been switched over to send encrypted bodies.
 */
function readEncryptedRequestBody() {
    $raw = file_get_contents('php://input');
    if ($raw === false || $raw === '') return [];
    $decoded = decryptPayload($raw);
    return is_array($decoded) ? $decoded : [];
}

// ==========================================================
// USER HELPERS
// ==========================================================

/**
 * Ensures a user record has the newer multi-API-key fields, migrating
 * an old single `api_key` string into the `api_keys` array format the
 * rest of the app expects. Safe to call repeatedly (no-op if already migrated).
 */
function migrateUserSchema($user) {
    if ($user === null) return null;

    if (!isset($user['api_keys']) || !is_array($user['api_keys'])) {
        $user['api_keys'] = [];
        if (!empty($user['api_key'])) {
            $user['api_keys'][] = [
                'key' => $user['api_key'],
                'label' => 'Default',
                'created_at' => $user['created_at'] ?? nowIST()->format('Y-m-d H:i:s')
            ];
        }
    }
    if (!isset($user['reset_chances'])) {
        $user['reset_chances'] = 10;
    }
    if (!isset($user['avatar'])) {
        $user['avatar'] = '';
    }
    return $user;
}

function getUserByEmail($email) {
    $users = readJson(USERS_FILE);
    foreach ($users as $user) {
        if (strtolower($user['email']) === strtolower($email)) {
            return migrateUserSchema($user);
        }
    }
    return null;
}

function getUserById($id) {
    $users = readJson(USERS_FILE);
    foreach ($users as $user) {
        if ($user['id'] === $id) {
            return migrateUserSchema($user);
        }
    }
    return null;
}

function saveUser($userData) {
    $users = readJson(USERS_FILE);
    $found = false;
    foreach ($users as $i => $user) {
        if ($user['id'] === $userData['id']) {
            $users[$i] = $userData;
            $found = true;
            break;
        }
    }
    if (!$found) {
        $users[] = $userData;
    }
    return writeJson(USERS_FILE, $users);
}

function generateUserId() {
    return 'USR' . strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 10));
}

function generateApiKey() {
    return 'fam_' . bin2hex(random_bytes(20));
}

/**
 * Generates a custom-named API key in the format: Name_fampayXXXX
 * (4 random digits). $rawName is slugified to alnum only.
 */
function generateCustomApiKey($rawName) {
    $clean = preg_replace('/[^A-Za-z0-9]/', '', $rawName);
    if ($clean === '') $clean = 'user';
    $digits = str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
    return $clean . '_fampay' . $digits;
}

/**
 * Searches across every user's api_keys array for a matching key.
 * Returns the user (migrated) or null.
 */
function getUserByApiKey($apiKey) {
    $users = readJson(USERS_FILE);
    foreach ($users as $user) {
        $user = migrateUserSchema($user);
        foreach ($user['api_keys'] as $k) {
            if (($k['key'] ?? '') === $apiKey) {
                return $user;
            }
        }
        // Backward-compat: very old records that never migrated on disk
        if (isset($user['api_key']) && $user['api_key'] === $apiKey) {
            return $user;
        }
    }
    return null;
}

// ==========================================================
// SESSION / AUTH HELPERS
// ==========================================================
// Sessions last 30 days from the moment of login and are refreshed on
// every request, so a user who keeps visiting effectively never gets
// logged out until they click Logout themselves.
define('SESSION_LIFETIME_SECONDS', 30 * 24 * 60 * 60); // 30 days

function startSecureSession() {
    if (session_status() === PHP_SESSION_NONE) {
        // 1) Tell the BROWSER to keep the cookie for 30 days.
        session_set_cookie_params([
            'lifetime' => SESSION_LIFETIME_SECONDS,
            'path' => '/',
            'secure' => isset($_SERVER['HTTPS']),
            'httponly' => true,
            'samesite' => 'Lax'
        ]);

        // 2) Tell PHP's SERVER-SIDE garbage collector to keep the session
        // file around for 30 days too. Without this, most hosts default
        // to expiring the session data after ~24 minutes of inactivity
        // (session.gc_maxlifetime), which silently logs people out even
        // though their browser cookie is still valid. This is very likely
        // what was causing the "logs out after a while" behavior.
        ini_set('session.gc_maxlifetime', (string) SESSION_LIFETIME_SECONDS);
        ini_set('session.cookie_lifetime', (string) SESSION_LIFETIME_SECONDS);
        // Lower the odds this session's file gets swept by another
        // request's garbage collection run before our 30 days are up.
        ini_set('session.gc_probability', '1');
        ini_set('session.gc_divisor', '1000');

        session_start();

        // 3) Re-issue (refresh) the cookie's expiry on every request that
        // starts a session, so "30 days" means 30 days from the user's
        // LAST visit, not 30 days from when they first logged in.
        if (isset($_SESSION['user_id']) || isset($_SESSION['is_admin'])) {
            $params = session_get_cookie_params();
            setcookie(session_name(), session_id(), [
                'expires' => time() + SESSION_LIFETIME_SECONDS,
                'path' => $params['path'],
                'domain' => $params['domain'],
                'secure' => $params['secure'],
                'httponly' => $params['httponly'],
                'samesite' => $params['samesite'] ?? 'Lax'
            ]);
        }
    }
}

function isLoggedIn() {
    return isset($_SESSION['user_id']);
}

function isAdminLoggedIn() {
    return isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
}

function requireLogin() {
    if (!isLoggedIn()) {
        header('Location: login.php');
        exit;
    }
}

function requireAdmin() {
    if (!isAdminLoggedIn()) {
        header('Location: login.php');
        exit;
    }
}

// ==========================================================
// PLAN HELPERS
// ==========================================================
function getPlans() {
    $plans = readJson(PLANS_FILE);
    if (empty($plans)) {
        // Default plans if file doesn't exist yet
        $plans = getDefaultPlans();
        writeJson(PLANS_FILE, $plans);
    }
    return $plans;
}

function getDefaultPlans() {
    return [
        [
            'id' => 'plan_5',
            'price' => 5,
            'days' => 3,
            'original_price' => null,
            'offer' => false,
            'label' => '3 Days'
        ],
        [
            'id' => 'plan_10',
            'price' => 10,
            'days' => 7,
            'original_price' => null,
            'offer' => false,
            'label' => '7 Days'
        ],
        [
            'id' => 'plan_30',
            'price' => 30,
            'days' => 25,
            'original_price' => null,
            'offer' => false,
            'label' => '25 Days'
        ],
        [
            'id' => 'plan_50',
            'price' => 50,
            'days' => 75,
            'original_price' => null,
            'offer' => false,
            'label' => '75 Days'
        ],
        [
            'id' => 'plan_100',
            'price' => 100,
            'days' => 150,
            'original_price' => null,
            'offer' => false,
            'label' => '150 Days'
        ],
        [
            'id' => 'plan_200',
            'price' => 200,
            'days' => 365,
            'original_price' => 500,
            'offer' => true,
            'label' => '365 Days - Special Offer'
        ]
    ];
}

function getPlanById($id) {
    $plans = getPlans();
    foreach ($plans as $plan) {
        if ($plan['id'] === $id) return $plan;
    }
    return null;
}

// ==========================================================
// ADMIN SETTINGS HELPERS (Gmail + UPI for receiving plan payments)
// ==========================================================
function getAdminSettings() {
    $settings = readJson(ADMIN_SETTINGS_FILE);
    if (empty($settings)) {
        $settings = [
            'gmail_id' => '',
            'app_password_encrypted' => '',
            'upi_id' => ''
        ];
    }
    // Defaults for the newer pricing fields, in case an older settings file
    // was saved before these existed.
    if (!isset($settings['custom_api_price'])) $settings['custom_api_price'] = 100;
    if (!isset($settings['reset_unlock_price'])) $settings['reset_unlock_price'] = 5;
    if (!isset($settings['base_url'])) $settings['base_url'] = '';
    if (!isset($settings['demo_amount'])) $settings['demo_amount'] = 1;
    return $settings;
}

/**
 * Returns the site's base URL for use in documentation and generated links.
 * Prefers the admin-configured value (Admin Panel → Settings → Base URL),
 * so moving to a new domain only requires updating that one field instead
 * of editing any file. Falls back to auto-detecting from the current
 * request if the admin hasn't set one yet.
 */
function getBaseUrl() {
    $settings = getAdminSettings();
    if (!empty($settings['base_url'])) {
        return rtrim($settings['base_url'], '/');
    }
    return 'https://adityahost.in';
}

function saveAdminSettings($gmailId, $appPassword, $upiId) {
    $settings = getAdminSettings();
    $settings['gmail_id'] = $gmailId;
    $settings['app_password_encrypted'] = encryptData($appPassword);
    $settings['upi_id'] = $upiId;
    return writeJson(ADMIN_SETTINGS_FILE, $settings);
}

// ==========================================================
// SHORTLINK HELPERS (JSON-file backed, no database — same pattern as
// the rest of this app's storage)
// ==========================================================
define('SHORTLINKS_FILE', DATA_DIR . '/shortlinks.json');
define('SHORTLINK_LIFETIME_SECONDS', 10 * 60); // payment links expire 10 minutes after creation

/**
 * Generates a short, URL-safe random code (6 chars, mixed-case + digits).
 * Retries on the rare collision against existing stored codes.
 */
function generateShortCode() {
    $alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789'; // no 0/O/1/l/I ambiguity
    $links = readJson(SHORTLINKS_FILE);
    do {
        $code = '';
        for ($i = 0; $i < 6; $i++) {
            $code .= $alphabet[random_int(0, strlen($alphabet) - 1)];
        }
    } while (isset($links[$code]));
    return $code;
}

/**
 * Stores a new shortlink mapping short code -> full target URL.
 * Expires SHORTLINK_LIFETIME_SECONDS (10 min) after creation — matches
 * the same 5-10 minute window the underlying payment order itself uses,
 * so a stale/reused link can't be paid against after the order it points
 * to has already timed out.
 * Returns the generated code.
 */
function createShortlink($targetUrl, $ownerUserId) {
    $links = readJson(SHORTLINKS_FILE);
    $code = generateShortCode();
    $links[$code] = [
        'code' => $code,
        'target_url' => $targetUrl,
        'owner_user_id' => $ownerUserId,
        'created_at' => nowIST()->format('Y-m-d H:i:s'),
        'created_at_timestamp' => time(),
        'expires_at_timestamp' => time() + SHORTLINK_LIFETIME_SECONDS,
        'clicks' => 0
    ];
    writeJson(SHORTLINKS_FILE, $links);
    return $code;
}

/**
 * Returns the shortlink record, or null if it doesn't exist OR has expired.
 * (Expired links are left in the file — not deleted — so admins can still
 * see them as "Expired" in the Payment Links admin page; see isExpired flag.)
 */
function getShortlink($code) {
    $links = readJson(SHORTLINKS_FILE);
    $link = $links[$code] ?? null;
    if (!$link) return null;

    $expiresAt = $link['expires_at_timestamp'] ?? 0;
    if ($expiresAt > 0 && time() > $expiresAt) {
        return null; // treat as gone — resolveShortlink()/admin page distinguish "expired" separately
    }
    return $link;
}

/**
 * Same lookup as getShortlink() but WITHOUT the expiry check — used by
 * the redirect handler to tell "expired" apart from "never existed", and
 * by the admin page to show expired links instead of hiding them.
 */
function getShortlinkRaw($code) {
    $links = readJson(SHORTLINKS_FILE);
    return $links[$code] ?? null;
}

function isShortlinkExpired($link) {
    if (!$link) return false;
    $expiresAt = $link['expires_at_timestamp'] ?? 0;
    return $expiresAt > 0 && time() > $expiresAt;
}

function incrementShortlinkClicks($code) {
    $links = readJson(SHORTLINKS_FILE);
    if (isset($links[$code])) {
        $links[$code]['clicks'] = ($links[$code]['clicks'] ?? 0) + 1;
        writeJson(SHORTLINKS_FILE, $links);
    }
}

/** All shortlinks created by a given user, newest first — used on the dashboard. */
function getShortlinksByUser($userId) {
    $links = readJson(SHORTLINKS_FILE);
    $mine = array_values(array_filter($links, fn($l) => ($l['owner_user_id'] ?? '') === $userId));
    usort($mine, fn($a, $b) => strcmp($b['created_at'], $a['created_at']));
    return $mine;
}
function jsonResponse($data, $httpCode = 200) {
    http_response_code($httpCode);
    header('Content-Type: application/json');
    echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    exit;
}

function nowIST() {
    $dt = new DateTime('now', new DateTimeZone('Asia/Kolkata'));
    return $dt;
}

// ==========================================================
// ORDER HELPERS (used by both dashboard "buy plan" QR and public API /qr, /verify_order)
// ==========================================================
function saveOrder($order) {
    $orders = readJson(ORDERS_FILE);
    $orders[] = $order;
    // Keep file from growing forever - retain last 5000 orders
    if (count($orders) > 5000) {
        $orders = array_slice($orders, -5000);
    }
    writeJson(ORDERS_FILE, $orders);
}

function updateOrder($orderId, $updates) {
    $orders = readJson(ORDERS_FILE);
    foreach ($orders as $i => $order) {
        if ($order['order_id'] === $orderId) {
            $orders[$i] = array_merge($order, $updates);
            writeJson(ORDERS_FILE, $orders);
            return true;
        }
    }
    return false;
}

function getOrderById($orderId) {
    $orders = readJson(ORDERS_FILE);
    foreach ($orders as $order) {
        if ($order['order_id'] === $orderId) return $order;
    }
    return null;
}

function generateOrderId($prefix = 'FAMPAY') {
    return $prefix . date('YmdHis') . strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 6));
}

/**
 * Generates a short human-typeable code (e.g. "FP7X9K2") meant to go in the
 * UPI transaction NOTE/remark field (the `tn=` param), not the `tr=` field.
 *
 * Why this exists: if two people generate a QR for the same amount at the
 * same time (e.g. two ₹100 orders), matching payment-confirmation emails by
 * amount alone can't tell them apart — whichever one pays first gets
 * matched to BOTH orders. The `tn` note is what shows up in the payer's UPI
 * app as the "remark", and FamPay's confirmation email includes it, so we
 * can match on amount AND this exact code together for a much safer check.
 */
function generatePaymentNoteCode() {
    $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // no 0/O/1/I to avoid confusion
    $code = 'FP';
    for ($i = 0; $i < 5; $i++) {
        $code .= $chars[random_int(0, strlen($chars) - 1)];
    }
    return $code;
}

/**
 * Computes a stable identifier for a pay.php checkout link, based only on
 * its parameters (api_key + amount + optional redirect/name). Same link ->
 * same id, every time it's opened.
 */
function computePayLinkId($apiKey, $amount, $redirectUrl = '', $linkName = '', $nonce = '') {
    return substr(hash('sha256', $apiKey . '|' . number_format((float)$amount, 2, '.', '') . '|' . $redirectUrl . '|' . $linkName . '|' . $nonce), 0, 20);
}

/**
 * Looks up the most recent order tied to a given pay-link id.
 * Used by pay.php to make a checkout link idempotent:
 *   - if that link already has a SUCCESSFUL order -> show "Already Paid", don't create a new one
 *   - if it has a still-valid PENDING order -> reuse it (don't spawn a duplicate QR/order)
 *   - otherwise -> caller creates a fresh order as normal
 */
function getLatestOrderByLinkId($linkId) {
    $orders = readJson(ORDERS_FILE);
    $match = null;
    foreach ($orders as $o) {
        if (($o['pay_link_id'] ?? '') !== $linkId) continue;
        if ($match === null || ($o['created_at_timestamp'] ?? 0) > ($match['created_at_timestamp'] ?? 0)) {
            $match = $o;
        }
    }
    return $match;
}

// ==========================================================
// FAMAPP GMAIL VERIFICATION CORE (IMAP-based)
// Shared by: dashboard plan-purchase checker AND public API /verify_order
// ==========================================================
function connectFamAppGmail($gmailId, $appPassword) {
    if (!function_exists('imap_open')) {
        return ['success' => false, 'error' => 'IMAP extension not enabled on server.'];
    }

    // Cut connect/read/write timeouts down (default can be 30s+) so a slow
    // network hop doesn't stall the whole request. 8s is generous for a
    // TLS+login round trip to Gmail but won't let it hang.
    @imap_timeout(IMAP_OPENTIMEOUT, 8);
    @imap_timeout(IMAP_READTIMEOUT, 8);
    @imap_timeout(IMAP_WRITETIMEOUT, 8);

    // OP_HALFOPEN-free fast open: skip novalidate-cert retry unless the first fails
    $inbox = @imap_open('{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX', $gmailId, $appPassword, 0, 1);
    if (!$inbox) {
        $inbox = @imap_open('{imap.gmail.com:993/imap/ssl}INBOX', $gmailId, $appPassword, 0, 1);
    }
    if (!$inbox) {
        return ['success' => false, 'error' => 'Could not connect to Gmail: ' . imap_last_error()];
    }
    return ['success' => true, 'inbox' => $inbox];
}

function getEmailBodyDecoded($inbox, $uid) {
    $structure = @imap_fetchstructure($inbox, $uid, FT_UID);

    $message = @imap_fetchbody($inbox, $uid, '1', FT_UID);
    if (empty(trim(strip_tags($message)))) {
        $message = @imap_fetchbody($inbox, $uid, '1.1', FT_UID);
    }
    if (empty(trim(strip_tags($message)))) {
        $message = @imap_body($inbox, $uid, FT_UID);
    }

    $partEncoding = null;
    if (isset($structure->parts) && is_array($structure->parts) && count($structure->parts) > 0) {
        $partEncoding = findTextPartEncodingRecursive($structure->parts);
    } elseif (isset($structure->encoding)) {
        $partEncoding = $structure->encoding;
    }

    if ($partEncoding == 3) {
        $decoded = base64_decode($message);
        if ($decoded !== false) $message = $decoded;
    } elseif ($partEncoding == 4) {
        $message = quoted_printable_decode($message);
    }

    // Safety net: force-decode if literal =XX hex escapes are still present
    if (preg_match('/=[0-9A-Fa-f]{2}/', $message) || strpos($message, "=\r\n") !== false || strpos($message, "=\n") !== false) {
        $message = quoted_printable_decode($message);
    }

    return strip_tags($message);
}

function findTextPartEncodingRecursive($parts) {
    foreach ($parts as $part) {
        if (isset($part->subtype) && (strtoupper($part->subtype) === 'PLAIN' || strtoupper($part->subtype) === 'HTML')) {
            return $part->encoding ?? null;
        }
        if (isset($part->parts) && is_array($part->parts)) {
            $found = findTextPartEncodingRecursive($part->parts);
            if ($found !== null) return $found;
        }
    }
    return null;
}

/**
 * Check a connected Gmail inbox for a FamPay payment matching $expectedAmount,
 * received AFTER $sinceTimestamp. Returns match details or pending status.
 */
function checkFamAppInboxForPayment($gmailId, $appPassword, $expectedAmount, $sinceTimestamp, $expectedNoteCode = null) {
    $conn = connectFamAppGmail($gmailId, $appPassword);
    if (!$conn['success']) {
        return ['status' => 'error', 'message' => $conn['error']];
    }
    $inbox = $conn['inbox'];

    // Narrow the search to only emails since the order was created (minus 1 day
    // buffer for IMAP's date-only SINCE granularity). This avoids Gmail scanning
    // and returning the whole historical match set on every single poll.
    $sinceDate = $sinceTimestamp > 0
        ? date('d-M-Y', $sinceTimestamp - 86400)
        : date('d-M-Y', time() - 86400);
    $emails = imap_search($inbox, 'FROM "no-reply@famapp.in" SINCE "' . $sinceDate . '"', SE_UID);
    if (!$emails) {
        imap_close($inbox);
        return ['status' => 'pending', 'message' => 'No FamPay emails found yet.'];
    }

    rsort($emails);
    $checkCount = 0;

    foreach ($emails as $uid) {
        // Since results are now already narrowed to "since order creation",
        // checking the 4 most recent is enough and cuts fetch/parse time
        // versus the old fixed limit of 10.
        if ($checkCount >= 4) break;
        $checkCount++;

        $headerInfo = @imap_headerinfo($inbox, imap_msgno($inbox, $uid));
        $emailTimestamp = ($headerInfo && isset($headerInfo->udate)) ? $headerInfo->udate : 0;

        // Skip emails older than when this order/QR was generated (10s buffer for clock skew)
        if ($sinceTimestamp > 0 && $emailTimestamp > 0 && $emailTimestamp < ($sinceTimestamp - 10)) {
            continue;
        }

        $body = getEmailBodyDecoded($inbox, $uid);

        if (preg_match('/received[^0-9]{0,15}([\d,]+\.?\d*)\s*from/iu', $body, $matches)) {
            $foundAmount = floatval(str_replace(',', '', $matches[1]));
            $expected = floatval($expectedAmount);

            if (abs($foundAmount - $expected) < 0.01) {
                // If this order has a unique note code (the newer flow), the
                // payer's remark/note MUST contain it — otherwise this could
                // just be a coincidental same-amount payment from someone
                // else (e.g. a family member sending ₹100 for something
                // unrelated at the same time another ₹100 QR is open).
                if ($expectedNoteCode !== null && $expectedNoteCode !== '') {
                    if (stripos($body, $expectedNoteCode) === false) {
                        continue; // amount matches but this isn't OUR payment — keep looking
                    }
                }

                $txnId = '';
                $utr = '';
                $senderName = '';
                if (preg_match('/transaction id\s*:?\s*([A-Za-z0-9]+)/i', $body, $m)) $txnId = $m[1];
                if (preg_match('/UTR\s*:?\s*(\d+)/i', $body, $m)) $utr = $m[1];
                if (preg_match('/from\s+([A-Z\s]+?)\s+at\s+\d{1,2}:\d{2}/i', $body, $m)) $senderName = trim($m[1]);

                imap_close($inbox);
                return [
                    'status' => 'success',
                    'amount' => $foundAmount,
                    'transaction_id' => $txnId,
                    'utr' => $utr,
                    'sender_name' => $senderName,
                    'payment_timestamp' => $emailTimestamp
                ];
            }
        }
    }

    imap_close($inbox);
    return ['status' => 'pending', 'message' => 'Payment not received yet.'];
}

// ==========================================================
// OTP HELPERS (for registration, login, password reset)
// ==========================================================
function generateOTP() {
    return str_pad((string)random_int(100000, 999999), 6, '0', STR_PAD_LEFT);
}

function saveOTP($email, $type = 'register') {
    $pdo = dbConnection();
    // Delete old OTPs for this email and type
    $stmt = $pdo->prepare('DELETE FROM otps WHERE email = ? AND type = ?');
    $stmt->execute([$email, $type]);
    
    $otp = generateOTP();
    
    $stmt = $pdo->prepare('INSERT INTO otps (email, otp, type, expires_at) VALUES (?, ?, ?, NOW() + INTERVAL 10 MINUTE)');
    $stmt->execute([$email, $otp, $type]);
    
    return $otp;
}

function verifyOTP($email, $otp, $type = 'register') {
    $pdo = dbConnection();
    $stmt = $pdo->prepare('SELECT * FROM otps WHERE email = ? AND otp = ? AND type = ? AND verified = 0 AND expires_at > NOW() ORDER BY id DESC LIMIT 1');
    $stmt->execute([$email, $otp, $type]);
    $record = $stmt->fetch();
    
    if ($record) {
        // Mark as verified
        $stmt = $pdo->prepare('UPDATE otps SET verified = 1 WHERE id = ?');
        $stmt->execute([$record['id']]);
        return true;
    }
    return false;
}

function getUserByUsername($username) {
    $pdo = dbConnection();
    $stmt = $pdo->prepare('SELECT * FROM users WHERE username = ?');
    $stmt->execute([$username]);
    $row = $stmt->fetch();
    if ($row) {
        return dbUserToArray($row);
    }
    return null;
}

function sendOTPEmail($email, $otp, $type = 'register') {
    require_once __DIR__ . '/../vendor/autoload.php';
    
    $smtpHost = 'mail.adityahost.in';
    $smtpPort = 465;
    $smtpUser = 'fampay@adityahost.in';
    $smtpPass = 'Aditya@2627';
    $smtpSecure = 'ssl';
    
    $subject = '';
    $message = '';
    
    switch ($type) {
        case 'register':
            $subject = 'FamAPI - Verify Your Email';
            $message = "
            <div style='font-family:Arial,sans-serif;max-width:500px;margin:0 auto;padding:20px;'>
                <div style='text-align:center;margin-bottom:20px;'>
                    <h2 style='color:#a370fc;margin:0;'>FamAPI</h2>
                    <p style='color:#666;font-size:14px;'>Email Verification</p>
                </div>
                <div style='background:#f8f9fa;border-radius:12px;padding:24px;text-align:center;'>
                    <p style='color:#333;font-size:16px;margin:0 0 16px;'>Your verification code is:</p>
                    <div style='font-size:32px;font-weight:700;color:#a370fc;letter-spacing:4px;margin:16px 0;'>{$otp}</div>
                    <p style='color:#666;font-size:13px;margin:16px 0 0;'>This code expires in 10 minutes.</p>
                </div>
                <p style='color:#999;font-size:12px;text-align:center;margin-top:20px;'>If you didn't request this, please ignore this email.</p>
            </div>";
            break;
        case 'login':
            $subject = 'FamAPI - Login Verification Code';
            $message = "
            <div style='font-family:Arial,sans-serif;max-width:500px;margin:0 auto;padding:20px;'>
                <div style='text-align:center;margin-bottom:20px;'>
                    <h2 style='color:#a370fc;margin:0;'>FamAPI</h2>
                    <p style='color:#666;font-size:14px;'>Login Verification</p>
                </div>
                <div style='background:#f8f9fa;border-radius:12px;padding:24px;text-align:center;'>
                    <p style='color:#333;font-size:16px;margin:0 0 16px;'>Your login code is:</p>
                    <div style='font-size:32px;font-weight:700;color:#a370fc;letter-spacing:4px;margin:16px 0;'>{$otp}</div>
                    <p style='color:#666;font-size:13px;margin:16px 0 0;'>This code expires in 10 minutes.</p>
                </div>
                <p style='color:#999;font-size:12px;text-align:center;margin-top:20px;'>If you didn't request this, please ignore this email.</p>
            </div>";
            break;
        case 'reset':
            $subject = 'FamAPI - Password Reset Code';
            $message = "
            <div style='font-family:Arial,sans-serif;max-width:500px;margin:0 auto;padding:20px;'>
                <div style='text-align:center;margin-bottom:20px;'>
                    <h2 style='color:#a370fc;margin:0;'>FamAPI</h2>
                    <p style='color:#666;font-size:14px;'>Password Reset</p>
                </div>
                <div style='background:#f8f9fa;border-radius:12px;padding:24px;text-align:center;'>
                    <p style='color:#333;font-size:16px;margin:0 0 16px;'>Your password reset code is:</p>
                    <div style='font-size:32px;font-weight:700;color:#a370fc;letter-spacing:4px;margin:16px 0;'>{$otp}</div>
                    <p style='color:#666;font-size:13px;margin:16px 0 0;'>This code expires in 10 minutes.</p>
                </div>
                <p style='color:#999;font-size:12px;text-align:center;margin-top:20px;'>If you didn't request this, please ignore this email.</p>
            </div>";
            break;
    }
    
    try {
        $mail = new PHPMailer\PHPMailer\PHPMailer(true);
        $mail->isSMTP();
        $mail->Host = $smtpHost;
        $mail->SMTPAuth = true;
        $mail->Username = $smtpUser;
        $mail->Password = $smtpPass;
        $mail->SMTPSecure = $smtpSecure;
        $mail->Port = $smtpPort;
        $mail->CharSet = 'UTF-8';
        
        $mail->setFrom($smtpUser, 'FamAPI');
        $mail->addAddress($email);
        
        $mail->isHTML(true);
        $mail->Subject = $subject;
        $mail->Body = $message;
        $mail->AltBody = "Your verification code is: {$otp}";
        
        $mail->send();
        return true;
    } catch (Exception $e) {
        error_log("OTP Email failed: " . $e->getMessage());
        return false;
    }
}

function isUsernameTaken($username) {
    $pdo = dbConnection();
    $stmt = $pdo->prepare('SELECT COUNT(*) FROM users WHERE username = ?');
    $stmt->execute([$username]);
    return $stmt->fetchColumn() > 0;
}

// ==========================================================
// WEBHOOK HELPERS
// ==========================================================
function sendWebhook($userId, $event, $data) {
    $user = getUserById($userId);
    if (!$user || empty($user['webhook_url'])) {
        return false;
    }
    
    $webhookUrl = $user['webhook_url'];
    if (!filter_var($webhookUrl, FILTER_VALIDATE_URL)) {
        return false;
    }
    
    $payload = json_encode([
        'event' => $event,
        'timestamp' => nowIST()->format('Y-m-d H:i:s'),
        'data' => $data
    ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    
    $ch = curl_init($webhookUrl);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $payload,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json',
            'X-FamAPI-Event: ' . $event,
            'X-FamAPI-Signature: ' . hash_hmac('sha256', $payload, ENCRYPTION_KEY)
        ],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_SSL_VERIFYPEER => true
    ]);
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    return ($httpCode >= 200 && $httpCode < 300);
}

function notifyPaymentReceived($order, $user) {
    $data = [
        'order_id' => $order['order_id'],
        'amount' => $order['amount'],
        'sender_name' => $order['sender_name'] ?? '',
        'utr' => $order['utr'] ?? '',
        'transaction_id' => $order['transaction_id'] ?? '',
        'payment_time' => $order['payment_time_ist'] ?? '',
        'upi_id' => $order['upi_id'] ?? ''
    ];
    
    return sendWebhook($user['id'], 'payment.received', $data);
}
