<?php
//Developer: core tech web designTechnology
//Whatsapp: +2349162671360
//Telegram: @jeditech

/**
 * smspva_functions.php
 *
 * All smspva.com API responses follow this envelope:
 *   { "statusCode": 200, "data": <payload> }
 *
 * Confirmed response shapes (from live debug):
 *
 * GET /activation/balance
 *   data: { "balance": "5.0000" }
 *
 * GET /activation/servicesprices
 *   data: [ {"price":"0.50","service":"opt0","serviceDescription":"...","country":"AL"}, ... ]
 *   — flat array, each item has "country" (ISO), "service" (code), "price" (string)
 *
 * GET /activation/serviceprice/{country}
 *   data: [ {"s":"opt0","sd":"desc","c":"RU","p":"0.50","po":{"Operator":"0.50",...}}, ... ]
 *   — flat array per service; "po" = per-operator prices object
 *
 * GET /activation/countnumbers/{country}
 *   data: [ {"operator":"Total_RU","country":"RU","services":[]}, ... ]
 *
 * GET /activation/number/{country}/{service}
 *   data: { "id": 123456, "number": "79001234567", "service": "opt0",
 *            "country": "RU", "operator": "MTS", "price": "0.50" }
 *
 * GET /activation/sms/{id}
 *   Waiting:  data: { "id": 123, "status": "wait" }
 *   Received: data: { "id": 123, "status": "ok", "sms": "Your code: 1234", "code": "1234" }
 *   Expired:  data: { "id": 123, "status": "cancel" }  OR statusCode: 404
 */

// No VALID_ACCESS check here — this file is included by both web scripts and CLI crons.

// ══════════════════════════════════════════════════════════════════
// CORE HTTP HELPER
// ══════════════════════════════════════════════════════════════════

/**
 * cURL GET against smspva.com — sends required "apikey" header.
 * Returns raw response string, or a JSON-encoded error object on failure.
 */
function smspva_curl_get(string $url, string $api_key, int $timeout = 30): string
{
    $headers = ['Accept: application/json', 'Connection: keep-alive'];
    if ($api_key !== '') {
        $headers[] = 'apikey: ' . $api_key;
    }

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => $timeout,
        CURLOPT_CONNECTTIMEOUT => 15,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => 0,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_USERAGENT      => 'Mozilla/5.0 (compatible; OTPBot/2.0)',
        CURLOPT_ENCODING       => '',
        CURLOPT_HTTPHEADER     => $headers,
    ]);
    $response = curl_exec($ch);
    $err      = curl_error($ch);
    $code     = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($response === false || $response === '') {
        return json_encode([
            'smspva_curl_error' => true,
            'http_code'         => $code,
            'message'           => $err ?: 'Empty response',
        ]);
    }
    return (string)$response;
}

// ══════════════════════════════════════════════════════════════════
// RESPONSE UNWRAPPER
// ══════════════════════════════════════════════════════════════════

/**
 * Unwrap the smspva envelope: {"statusCode":200,"data":<payload>}
 * Returns the inner "data" value, or false on error.
 *
 * Handles both the envelope format and bare responses (legacy fallback).
 */
function smspva_unwrap($raw_or_decoded)
{
    $dec = is_string($raw_or_decoded) ? json_decode($raw_or_decoded, true) : $raw_or_decoded;

    if (!is_array($dec)) return false;

    // Detect curl-level error
    if (isset($dec['smspva_curl_error'])) return false;

    // Envelope format: {"statusCode":N,"data":...}
    if (isset($dec['statusCode'])) {
        $code = (int)$dec['statusCode'];
        if ($code !== 200) return false;
        return $dec['data'] ?? false;
    }

    // No envelope — return as-is (some endpoints may respond without it)
    return $dec;
}

// ══════════════════════════════════════════════════════════════════
// URL BUILDER
// ══════════════════════════════════════════════════════════════════

function smspva_url(string $api_base, string $path): string
{
    return rtrim($api_base, '/') . '/' . ltrim($path, '/');
}

// ══════════════════════════════════════════════════════════════════
// ACCOUNT
// ══════════════════════════════════════════════════════════════════

/**
 * Get smspva balance.
 * Real response: {"statusCode":200,"data":{"balance":"5.0000"}}
 * Returns float or false.
 */
function smspva_get_balance(string $api_base, string $api_key)
{
    $result = smspva_get_balance_debug($api_base, $api_key);
    return $result['balance'];
}

/**
 * Balance with debug info for admin panel.
 * Returns ['balance'=>float|false, 'raw'=>string, 'decoded'=>array|null]
 */
function smspva_get_balance_debug(string $api_base, string $api_key): array
{
    $url = smspva_url($api_base, 'activation/balance');
    $raw = smspva_curl_get($url, $api_key, 20);
    $dec = json_decode($raw, true);

    $balance = false;
    $data    = smspva_unwrap($dec);

    if (is_array($data)) {
        // {"balance":"5.0000"}
        foreach ($data as $k => $v) {
            if (strtolower((string)$k) === 'balance' && is_numeric($v)) {
                $balance = (float)$v;
                break;
            }
        }
    } elseif (is_numeric($data)) {
        // bare numeric
        $balance = (float)$data;
    }

    return [
        'balance' => $balance,
        'raw'     => substr($raw, 0, 600),
        'decoded' => $dec,
    ];
}

// ══════════════════════════════════════════════════════════════════
// CATALOGUE HELPERS
// ══════════════════════════════════════════════════════════════════

/**
 * Get all service prices for a country.
 * GET /activation/serviceprice/{country}
 *
 * Real response data shape:
 * [
 *   {"s":"opt0","sd":"myopinions & erewards","c":"RU","p":"0.50",
 *    "po":{"Lyca_RU":"0.50","Yota_RU":"0.50","Beeline_RU":"0.50",...}},
 *   ...
 * ]
 *
 * Returns array indexed by service_code:
 * [
 *   'opt0' => ['service_code'=>'opt0','service_name'=>'...','operators'=>[
 *     ['operator'=>'Lyca_RU','price'=>0.50,'count'=>0], ...
 *   ]],
 *   ...
 * ]
 * or false on error.
 */
function smspva_get_country_prices(string $api_base, string $api_key, string $country_iso)
{
    $url  = smspva_url($api_base, 'activation/serviceprice/' . urlencode(strtoupper($country_iso)));
    $raw  = smspva_curl_get($url, $api_key, 60);
    $data = smspva_unwrap($raw);

    if (!is_array($data) || empty($data)) return false;

    $result = [];

    foreach ($data as $entry) {
        if (!is_array($entry)) continue;

        // Field names: s=service_code, sd=description, c=country, p=min_price, po=per_operator prices
        $svc_code = trim($entry['s']  ?? $entry['service'] ?? $entry['service_code'] ?? '');
        $svc_name = trim($entry['sd'] ?? $entry['serviceDescription'] ?? $entry['name'] ?? $svc_code);
        $min_price = (float)($entry['p'] ?? $entry['price'] ?? 0);

        if ($svc_code === '') continue;

        // Build operator list from "po" object: {"OperatorName": "price_string", ...}
        $ops = [];
        $po  = $entry['po'] ?? $entry['operators'] ?? [];
        if (is_array($po)) {
            foreach ($po as $op_name => $op_price) {
                if (is_numeric($op_price) || (is_string($op_price) && is_numeric($op_price))) {
                    $ops[] = [
                        'operator' => (string)$op_name,
                        'price'    => (float)$op_price,
                        'count'    => 999, // smspva doesn't give stock in this endpoint
                    ];
                }
            }
        }

        // If no per-operator breakdown, create one synthetic entry with the min price
        if (empty($ops) && $min_price > 0) {
            $ops[] = [
                'operator' => '',
                'price'    => $min_price,
                'count'    => 999,
            ];
        }

        $result[$svc_code] = [
            'service_code' => $svc_code,
            'service_name' => $svc_name,
            'operators'    => $ops,
        ];
    }

    return empty($result) ? false : $result;
}

/**
 * Get cheapest available price for a specific country+service.
 * Returns ['price'=>float, 'operator'=>string, 'count'=>int] or false.
 */
function smspva_cheapest_option(string $api_base, string $api_key, string $country_iso, string $service_code)
{
    $prices = smspva_get_country_prices($api_base, $api_key, $country_iso);
    if (!$prices || !isset($prices[$service_code])) return false;

    $ops = $prices[$service_code]['operators'] ?? [];
    if (empty($ops)) return false;

    // Sort by price, pick cheapest
    usort($ops, function($a, $b) { return $a['price'] < $b['price'] ? -1 : ($a['price'] > $b['price'] ? 1 : 0); });
    $best = $ops[0];
    if ($best['price'] <= 0) return false;

    return [
        'price'    => $best['price'],
        'operator' => $best['operator'],
        'count'    => $best['count'],
    ];
}

/**
 * Fetch live USD price for a service in a country (cheapest operator).
 * Returns float or false.
 */
function smspva_fetch_live_price(string $api_base, string $api_key, string $country_iso, string $service_code)
{
    $opt = smspva_cheapest_option($api_base, $api_key, $country_iso, $service_code);
    return $opt ? $opt['price'] : false;
}

/**
 * Parse servicesprices flat array to extract unique country ISOs.
 * GET /activation/servicesprices
 *
 * Real data: [{"price":"0.50","service":"opt0","serviceDescription":"...","country":"AL"}, ...]
 * Returns ['AL'=>'Albania', 'RU'=>'Russia', ...] using ISO as name (admin renames).
 * Or false on failure.
 */
function smspva_get_countries_from_servicesprices(string $api_base, string $api_key)
{
    $url  = smspva_url($api_base, 'activation/servicesprices');
    $raw  = smspva_curl_get($url, $api_key, 90);
    $data = smspva_unwrap($raw);

    if (!is_array($data) || empty($data)) return false;

    $countries  = [];
    $name_map   = smspva_iso_to_country_name();

    foreach ($data as $entry) {
        if (!is_array($entry)) continue;
        // Field "country" contains the ISO code directly
        $iso = strtoupper(trim($entry['country'] ?? $entry['c'] ?? ''));
        if (strlen($iso) === 2 && ctype_alpha($iso)) {
            if (!isset($countries[$iso])) {
                $countries[$iso] = $name_map[$iso] ?? $iso;
            }
        }
    }

    return empty($countries) ? false : $countries;
}

/**
 * Get available number counts per operator for a country.
 * GET /activation/countnumbers/{country}
 *
 * Real data: [{"operator":"Total_RU","country":"RU","services":[]}, ...]
 * Returns ['OperatorName' => count, ...] or empty array.
 */
function smspva_count_numbers(string $api_base, string $api_key, string $country_iso): array
{
    $url  = smspva_url($api_base, 'activation/countnumbers/' . urlencode(strtoupper($country_iso)));
    $raw  = smspva_curl_get($url, $api_key, 20);
    $data = smspva_unwrap($raw);

    if (!is_array($data)) return [];

    $result = [];
    foreach ($data as $entry) {
        if (!is_array($entry)) continue;
        $op  = trim($entry['operator'] ?? $entry['name'] ?? '');
        $cnt = (int)($entry['count'] ?? $entry['qty'] ?? count($entry['services'] ?? []));
        if ($op !== '') {
            $result[$op] = $cnt;
        }
    }
    return $result;
}

// ══════════════════════════════════════════════════════════════════
// NUMBER PURCHASE
// ══════════════════════════════════════════════════════════════════

/**
 * Buy a number from smspva.com.
 * GET /activation/number/{country}/{service}[?operator=...]
 *
 * Real success data: {"id":123456,"number":"79001234567","service":"opt0",
 *                     "country":"RU","operator":"MTS","price":"0.50"}
 *
 * Returns ['success'=>bool, 'order_id'=>int, 'number'=>string,
 *          'operator'=>string, 'price'=>float, 'error'=>string]
 */
function smspva_buy_number(
    string $api_base,
    string $api_key,
    string $country_iso,
    string $service_code,
    string $operator = ''
): array {
    $path = 'activation/number/' . urlencode(strtoupper($country_iso)) . '/' . urlencode($service_code);
    if ($operator !== '') {
        $path .= '?operator=' . urlencode($operator);
    }

    $url = smspva_url($api_base, $path);
    $raw = smspva_curl_get($url, $api_key, 35);
    $dec = json_decode($raw, true);

    // cURL-level error
    if (isset($dec['smspva_curl_error'])) {
        return [
            'success'   => false, 'order_id' => 0, 'number' => '',
            'operator'  => '', 'price' => 0.0, 'http_code' => 0,
            'error'     => 'Connection error: ' . ($dec['message'] ?? 'Unknown'),
        ];
    }

    // Try to unwrap envelope
    $data = smspva_unwrap($dec);

    // If unwrap failed check statusCode for error message
    if ($data === false) {
        $status_code = (int)($dec['statusCode'] ?? 0);
        $error = smspva_parse_error($dec, $status_code);
        return [
            'success'   => false, 'order_id' => 0, 'number' => '',
            'operator'  => '', 'price' => 0.0, 'http_code' => $status_code,
            'error'     => $error,
        ];
    }

    // smspva data can be an object {"id":...,"number":...}
    // OR an array [{"id":...,"number":...}] — handle both shapes.
    // Also fall back to the bare decoded response for backward compat.
    if (is_array($data) && isset($data[0])) {
        // Array wrapper — unwrap first element
        $payload = $data[0];
    } elseif (is_array($data)) {
        $payload = $data;
    } else {
        $payload = is_array($dec) ? $dec : [];
    }

    // smspva buy response field names (confirmed from live API response):
    // orderId      → order identifier
    // phoneNumber  → the phone number (NOT 'number')
    // countryCode  → dial prefix e.g. "+1"
    $order_id = (int)($payload['orderId']     ?? $payload['id']       ??
                      $payload['order_id']    ?? $payload['Id']       ?? 0);
    $number   = trim($payload['phoneNumber']  ?? $payload['number']   ??
                     $payload['num']          ?? $payload['phone']    ??
                     $payload['Number']       ?? '');

    if ($order_id > 0 && $number !== '') {
        // Prepend country dial code if number doesn't already include it
        // e.g. countryCode="+1", phoneNumber="9715144770" → "+19715144770"
        $country_code = trim($payload['countryCode'] ?? $payload['country_code'] ?? '');
        if ($country_code !== '' && strpos($number, '+') !== 0 && strpos($number, $country_code) !== 0) {
            $number = $country_code . $number;
        }
        // Ensure leading + is present
        if (strpos($number, '+') !== 0 && is_numeric(ltrim($number, '+'))) {
            $number = '+' . ltrim($number, '+');
        }

        return [
            'success'   => true,
            'order_id'  => $order_id,
            'number'    => $number,
            'operator'  => (string)($payload['operator'] ?? $payload['op']       ??
                                    $payload['Operator'] ?? ''),
            'price'     => (float)($payload['price']    ?? $payload['p']         ??
                                   $payload['Price']    ?? 0),
            'http_code' => 200,
            'error'     => '',
        ];
    }

    // Number/id still not found — return full raw response in error for diagnosis
    $status_code = (int)($dec['statusCode'] ?? 0);
    $raw_preview = json_encode($payload ?: $dec);
    $error = 'smspva returned statusCode 200 but no number/id found. '
           . 'Raw data: ' . substr($raw_preview, 0, 300);
    return [
        'success'   => false, 'order_id' => 0, 'number' => '',
        'operator'  => '', 'price' => 0.0, 'http_code' => $status_code,
        'error'     => $error,
    ];
}

// ══════════════════════════════════════════════════════════════════
// SMS POLLING
// ══════════════════════════════════════════════════════════════════

/**
 * Poll smspva.com for an SMS.
 * GET /activation/sms/{id}
 *
 * Real data shapes:
 *   Waiting:  {"id":123,"status":"wait"}
 *   Received: {"id":123,"status":"ok","sms":"Your code: 1234","code":"1234"}
 *   Expired:  {"id":123,"status":"cancel"} OR statusCode 404
 *
 * Returns ['status'=>'received'|'waiting'|'expired'|'error',
 *          'sms'=>string, 'code'=>string, 'message'=>string]
 */
function smspva_get_sms(string $api_base, string $api_key, int $order_id): array
{
    $url = smspva_url($api_base, 'activation/sms/' . $order_id);
    $raw = smspva_curl_get($url, $api_key, 25);
    $dec = json_decode($raw, true);

    // cURL error
    if (isset($dec['smspva_curl_error'])) {
        return ['status' => 'waiting', 'sms' => '', 'code' => '', 'message' => 'Connection error, retrying...'];
    }

    // 404 = expired/not found
    if (isset($dec['statusCode']) && (int)$dec['statusCode'] === 404) {
        return ['status' => 'expired', 'sms' => '', 'code' => '', 'message' => 'Order not found or expired.'];
    }

    $data = smspva_unwrap($dec);

    // Unwrap failed but statusCode is not 200
    if ($data === false) {
        $sc = (int)($dec['statusCode'] ?? 0);
        if ($sc >= 400) {
            return ['status' => 'expired', 'sms' => '', 'code' => '', 'message' => 'Order error (code ' . $sc . ').'];
        }
        return ['status' => 'waiting', 'sms' => '', 'code' => '', 'message' => 'Waiting for SMS.'];
    }

    if (!is_array($data)) {
        return ['status' => 'waiting', 'sms' => '', 'code' => '', 'message' => 'Waiting for SMS.'];
    }

    $status = strtolower(trim($data['status'] ?? ''));

    // SMS received
    if ($status === 'ok' || $status === 'received' || !empty($data['sms']) || !empty($data['code'])) {

        // smspva 'sms' field can be a string OR an array — handle both.
        // Observed shapes:
        //   string: "Your code is 1234"
        //   array:  ["Your code is 1234"]
        //   array:  {"text":"Your code is 1234", "code":"1234"}
        $raw_sms = $data['sms'] ?? '';
        if (is_array($raw_sms)) {
            // Indexed array: ["msg text"] → take first element
            if (isset($raw_sms[0])) {
                $raw_sms = (string)$raw_sms[0];
            // Associative: {"text":"...", "code":"..."} → prefer 'text'
            } elseif (isset($raw_sms['text'])) {
                $raw_sms = (string)$raw_sms['text'];
            } elseif (isset($raw_sms['message'])) {
                $raw_sms = (string)$raw_sms['message'];
            } else {
                $raw_sms = implode(' ', array_map('strval', $raw_sms));
            }
        }
        $sms = trim((string)$raw_sms);

        // 'code' field can also be an array
        $raw_code = $data['code'] ?? '';
        if (is_array($raw_code)) {
            $raw_code = isset($raw_code[0]) ? (string)$raw_code[0] : '';
        }
        $code = trim((string)$raw_code);

        // Extract numeric OTP from SMS text if code field is empty
        if (empty($code) && !empty($sms)) {
            $code = smspva_extract_code($sms);
        }

        // Use code as the displayed value if SMS text is empty
        $display = $sms ?: $code;

        return [
            'status'  => 'received',
            'sms'     => $display,
            'code'    => $code,
            'message' => 'SMS received.',
        ];
    }

    // Expired/cancelled
    if (in_array($status, ['cancel', 'cancelled', 'expired', 'closed', 'error'], true)) {
        return ['status' => 'expired', 'sms' => '', 'code' => '', 'message' => 'Order expired or cancelled.'];
    }

    // Still waiting (status = 'wait' or anything else)
    return ['status' => 'waiting', 'sms' => '', 'code' => '', 'message' => 'Waiting for SMS.'];
}

// ══════════════════════════════════════════════════════════════════
// PRICING HELPERS
// ══════════════════════════════════════════════════════════════════

function smspva_get_markup(PDO $pdo): float
{
    try {
        $stmt = $pdo->prepare(
            "SELECT setting_value FROM price_settings
              WHERE setting_key IN ('smspva_markup_percent','markup_percent')
              ORDER BY FIELD(setting_key,'smspva_markup_percent','markup_percent')
              LIMIT 1"
        );
        $stmt->execute();
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return ($row && is_numeric($row['setting_value'])) ? (float)$row['setting_value'] : 35.0;
    } catch (PDOException $e) {
        return 35.0;
    }
}

function smspva_convert_price(float $usd_price, PDO $pdo): float
{
    $usd_to_ngn = otp_get_usd_to_ngn($pdo);
    $markup_pct = smspva_get_markup($pdo);
    return round($usd_price * $usd_to_ngn * (1 + $markup_pct / 100), 2);
}

// ══════════════════════════════════════════════════════════════════
// DB HELPERS
// ══════════════════════════════════════════════════════════════════

/**
 * Get the cached NGN price for a specific operator/service/country combination.
 * Falls back to the cheapest available price if the exact operator is not found.
 * Returns float > 0, or 0.0 if nothing cached.
 */
function smspva_operator_ngn_price(string $service_code, string $country_code, string $operator, PDO $pdo): float
{
    try {
        // Try exact operator match first
        if ($operator !== '' && $operator !== 'any') {
            $stmt = $pdo->prepare(
                "SELECT ngn_price FROM smspva_service_cache
                  WHERE service_code = :sc AND country_code = :cc AND operator = :op
                  LIMIT 1"
            );
            $stmt->execute([':sc' => $service_code, ':cc' => $country_code, ':op' => $operator]);
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
            if ($row && (float)$row['ngn_price'] > 0) return (float)$row['ngn_price'];
        }

        // Fall back to cheapest cached price for this service+country
        $stmt = $pdo->prepare(
            "SELECT ngn_price FROM smspva_service_cache
              WHERE service_code = :sc AND country_code = :cc AND ngn_price > 0
              ORDER BY ngn_price ASC LIMIT 1"
        );
        $stmt->execute([':sc' => $service_code, ':cc' => $country_code]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return ($row && (float)$row['ngn_price'] > 0) ? (float)$row['ngn_price'] : 0.0;
    } catch (PDOException $e) {
        return 0.0;
    }
}

/**
 * Get the cached USD price for a specific operator/service/country.
 * Returns float, or 0.0 if not found.
 */
function smspva_operator_usd_price(string $service_code, string $country_code, string $operator, PDO $pdo): float
{
    try {
        if ($operator !== '' && $operator !== 'any') {
            $stmt = $pdo->prepare(
                "SELECT usd_price FROM smspva_service_cache
                  WHERE service_code = :sc AND country_code = :cc AND operator = :op
                  LIMIT 1"
            );
            $stmt->execute([':sc' => $service_code, ':cc' => $country_code, ':op' => $operator]);
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
            if ($row && (float)$row['usd_price'] > 0) return (float)$row['usd_price'];
        }
        // Cheapest fallback
        $stmt = $pdo->prepare(
            "SELECT usd_price FROM smspva_service_cache
              WHERE service_code = :sc AND country_code = :cc AND usd_price > 0
              ORDER BY usd_price ASC LIMIT 1"
        );
        $stmt->execute([':sc' => $service_code, ':cc' => $country_code]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return ($row && (float)$row['usd_price'] > 0) ? (float)$row['usd_price'] : 0.0;
    } catch (PDOException $e) {
        return 0.0;
    }
}

function smspva_server_provider(int $server_id, PDO $pdo): string
{
    try {
        $stmt = $pdo->prepare(
            "SELECT ad.api_type FROM otp_server os
               JOIN api_detail ad ON ad.id = os.api_id
              WHERE os.id = :id LIMIT 1"
        );
        $stmt->execute([':id' => $server_id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ? (string)$row['api_type'] : 'smscodes';
    } catch (PDOException $e) {
        return 'smscodes';
    }
}

function smspva_is_enabled(PDO $pdo): bool
{
    try {
        $stmt = $pdo->prepare("SELECT setting_value FROM price_settings WHERE setting_key='smspva_api_enabled' LIMIT 1");
        $stmt->execute();
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ? ($row['setting_value'] === '1') : true;
    } catch (PDOException $e) {
        return true;
    }
}

function smspva_get_api_detail(PDO $pdo)
{
    try {
        $stmt = $pdo->prepare("SELECT * FROM api_detail WHERE api_type = 'smspva' LIMIT 1");
        $stmt->execute();
        return $stmt->fetch(PDO::FETCH_ASSOC);
    } catch (PDOException $e) {
        return false;
    }
}

// ══════════════════════════════════════════════════════════════════
// UTILITY
// ══════════════════════════════════════════════════════════════════

function smspva_extract_code(string $sms_text): string
{
    if (preg_match_all('/\b(\d{4,8})\b/', $sms_text, $matches)) {
        return end($matches[1]);
    }
    return trim($sms_text);
}

function smspva_parse_error(array $dec, int $status_code = 0): string
{
    // Check inside data for error messages
    $data = $dec['data'] ?? [];
    if (is_array($data)) {
        if (!empty($data['message'])) return (string)$data['message'];
        if (!empty($data['error']))   return (string)$data['error'];
        if (!empty($data['description'])) return (string)$data['description'];
    }
    if (!empty($dec['message'])) return (string)$dec['message'];
    if (!empty($dec['error']))   return (string)$dec['error'];

    $map = [
        400 => 'Bad request — invalid parameters.',
        401 => 'Unauthorised — check your API key.',
        402 => 'Insufficient balance to complete this purchase.',
        404 => 'Resource not found or order expired.',
        429 => 'Too many requests — please wait and try again.',
        500 => 'smspva server error — no number available.',
        503 => 'smspva server overloaded — please try again shortly.',
    ];
    if (isset($map[$status_code])) return $map[$status_code];

    return 'smspva error (statusCode ' . ($status_code ?: ($dec['statusCode'] ?? '?')) . ').';
}

function smspva_buy_error_message(int $status_code = 0, string $raw_error = ''): string
{
    // Don't expose raw API errors to users
    $map = [
        402 => 'Service temporarily unavailable. Please try again later.',
        404 => 'No numbers available for this service right now.',
        429 => 'Too many requests. Please wait a moment and try again.',
        500 => 'No numbers available. Please try another country or service.',
        503 => 'Server busy. Please try again in a few seconds.',
    ];
    return $map[$status_code] ?? 'Could not get a number. Please try again.';
}

function smspva_format_number(string $number, string $country_iso = ''): string
{
    $num = preg_replace('/\D/', '', $number);
    if (empty($num)) return $number;
    if ($number[0] === '+') return '+' . $num;
    if (strlen($num) < 7) return $number;
    return '+' . $num;
}

// ══════════════════════════════════════════════════════════════════
// ISO → COUNTRY NAME MAP  (used when building country list)
// ══════════════════════════════════════════════════════════════════

function smspva_iso_to_country_name(): array
{
    return [
        'AF'=>'Afghanistan','AL'=>'Albania','DZ'=>'Algeria','AO'=>'Angola','AR'=>'Argentina',
        'AM'=>'Armenia','AU'=>'Australia','AT'=>'Austria','AZ'=>'Azerbaijan','BH'=>'Bahrain',
        'BD'=>'Bangladesh','BY'=>'Belarus','BE'=>'Belgium','BJ'=>'Benin','BO'=>'Bolivia',
        'BA'=>'Bosnia and Herzegovina','BW'=>'Botswana','BR'=>'Brazil','BF'=>'Burkina Faso',
        'BI'=>'Burundi','KH'=>'Cambodia','CM'=>'Cameroon','CA'=>'Canada','CF'=>'Central African Republic',
        'CL'=>'Chile','CN'=>'China','CO'=>'Colombia','CD'=>'DR Congo','CR'=>'Costa Rica',
        'HR'=>'Croatia','CU'=>'Cuba','CZ'=>'Czech Republic','DK'=>'Denmark','DO'=>'Dominican Republic',
        'EC'=>'Ecuador','EG'=>'Egypt','SV'=>'El Salvador','EE'=>'Estonia','ET'=>'Ethiopia',
        'FJ'=>'Fiji','FI'=>'Finland','FR'=>'France','GA'=>'Gabon','GM'=>'Gambia',
        'GE'=>'Georgia','DE'=>'Germany','GH'=>'Ghana','GR'=>'Greece','GT'=>'Guatemala',
        'GN'=>'Guinea','GW'=>'Guinea-Bissau','GY'=>'Guyana','HN'=>'Honduras','HK'=>'Hong Kong',
        'HU'=>'Hungary','IN'=>'India','ID'=>'Indonesia','IR'=>'Iran','IQ'=>'Iraq',
        'IE'=>'Ireland','IL'=>'Israel','IT'=>'Italy','JM'=>'Jamaica','JP'=>'Japan',
        'JO'=>'Jordan','KZ'=>'Kazakhstan','KE'=>'Kenya','KM'=>'Comoros','KR'=>'South Korea',
        'KW'=>'Kuwait','KG'=>'Kyrgyzstan','LA'=>'Laos','LV'=>'Latvia','LB'=>'Lebanon',
        'LS'=>'Lesotho','LY'=>'Libya','LT'=>'Lithuania','MG'=>'Madagascar','MW'=>'Malawi',
        'MY'=>'Malaysia','ML'=>'Mali','MT'=>'Malta','MR'=>'Mauritania','MU'=>'Mauritius',
        'MX'=>'Mexico','MD'=>'Moldova','MN'=>'Mongolia','MA'=>'Morocco','MZ'=>'Mozambique',
        'MM'=>'Myanmar','NA'=>'Namibia','NP'=>'Nepal','NL'=>'Netherlands','NI'=>'Nicaragua',
        'NE'=>'Niger','NG'=>'Nigeria','MK'=>'North Macedonia','NO'=>'Norway','OM'=>'Oman',
        'PK'=>'Pakistan','PA'=>'Panama','PG'=>'Papua New Guinea','PY'=>'Paraguay','PE'=>'Peru',
        'PH'=>'Philippines','PL'=>'Poland','PT'=>'Portugal','QA'=>'Qatar','RO'=>'Romania',
        'RU'=>'Russia','RW'=>'Rwanda','SA'=>'Saudi Arabia','SN'=>'Senegal','RS'=>'Serbia',
        'SL'=>'Sierra Leone','SK'=>'Slovakia','SI'=>'Slovenia','SO'=>'Somalia',
        'ZA'=>'South Africa','ES'=>'Spain','LK'=>'Sri Lanka','SD'=>'Sudan','SZ'=>'Eswatini',
        'SE'=>'Sweden','CH'=>'Switzerland','SY'=>'Syria','TJ'=>'Tajikistan','TZ'=>'Tanzania',
        'TH'=>'Thailand','TG'=>'Togo','TT'=>'Trinidad and Tobago','TN'=>'Tunisia',
        'TR'=>'Turkey','TM'=>'Turkmenistan','UG'=>'Uganda','UA'=>'Ukraine',
        'AE'=>'United Arab Emirates','GB'=>'United Kingdom','UK'=>'United Kingdom',
        'US'=>'United States','UY'=>'Uruguay','UZ'=>'Uzbekistan','VE'=>'Venezuela',
        'VN'=>'Vietnam','YE'=>'Yemen','ZM'=>'Zambia','ZW'=>'Zimbabwe',
        'BZ'=>'Belize','CW'=>'Curacao','GY'=>'Guyana','JO'=>'Jordan','NC'=>'New Caledonia',
        'PR'=>'Puerto Rico','SB'=>'Solomon Islands','TO'=>'Tonga','SC'=>'Seychelles',
    ];
}