<?php
/**
 * SwapWallet Merchant API helper for GEM ACC.
 * Uses temporary-wallet invoices.
 * Official endpoint base: https://swapwallet.app/api
 */

if (!function_exists('sw_setting')) {
    function sw_setting(string $key, $default = '') {
        if (function_exists('getSetting')) return getSetting($key, $default);
        return $default;
    }
}

function sw_value_to_text($value): string {
    if ($value === null) return '';
    if (is_scalar($value)) return (string)$value;
    return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) ?: 'پاسخ نامعتبر';
}

function sw_base_url(): string {
    return rtrim((string)sw_setting('swapwallet_base_url', 'https://swapwallet.app/api'), '/');
}

function sw_invoice_endpoint(): string {
    $configured = trim((string)sw_setting('swapwallet_invoice_endpoint', ''));
    if ($configured !== '') {
        $username = sw_payment_username();
        if ($username !== '' && strpos($configured, '{username}') !== false) {
            return str_replace('{username}', rawurlencode($username), $configured);
        }
        return $configured;
    }

    // Current official merchant invoice endpoint. The public docs show this endpoint
    // with Authorization: Bearer business-... for shop keys. Some older apikey-...
    // accounts also accept it, while the legacy username endpoint is unreliable and
    // often returns HTTP 500. Therefore this endpoint is the safe default.
    return sw_base_url() . '/v1/merchants/invoices/temporary-wallet';
}

function sw_api_key(): string {
    // Prefer the secure server-side environment key when the DB setting is empty.
    // In older installs the setting row exists with an empty value, which made env() unreachable.
    $dbKey = trim((string)sw_setting('swapwallet_api_key', ''));
    $envKey = function_exists('env') ? trim((string)env('SWAPWALLET_API_KEY', '')) : '';
    $key = $dbKey !== '' ? $dbKey : $envKey;
    $key = trim($key, " \t\n\r\0\x0B\"'");

    // Small recovery for keys accidentally pasted without the leading "api" in the admin panel.
    if (strpos($key, 'key-') === 0) $key = 'api' . $key;
    return $key;
}

function sw_key_mode(): string {
    $key = sw_api_key();
    if (strpos($key, 'business-') === 0) return 'business';
    if (strpos($key, 'apikey-') === 0) return 'legacy';
    return 'unknown';
}

function sw_payment_username(): string {
    $v = trim((string)sw_setting('swapwallet_payment_username', ''));
    if ($v === '') $v = trim((string)sw_setting('swapwallet_username', ''));
    if ($v === '') $v = trim((string)sw_setting('swapwallet_shop_username', ''));
    $v = ltrim($v, '@');
    return $v;
}

function sw_default_token(): string {
    $v = trim((string)sw_setting('swapwallet_default_token', 'USDT'));
    return $v !== '' ? strtoupper($v) : 'USDT';
}

function sw_default_network(): string {
    $v = trim((string)sw_setting('swapwallet_default_network', 'TRON'));
    return $v !== '' ? strtoupper($v) : 'TRON';
}

function sw_amount_unit(): string {
    $v = trim((string)sw_setting('swapwallet_amount_unit', 'IRT'));
    return $v !== '' ? strtoupper($v) : 'IRT';
}

function sw_webhook_url(string $type = 'order'): string {
    $key = $type === 'wallet' ? 'swapwallet_wallet_webhook_url' : 'swapwallet_webhook_url';
    $configured = trim((string)sw_setting($key, ''));
    if ($configured !== '') return $configured;
    $file = $type === 'wallet' ? 'swapwallet_wallet_webhook.php' : 'swapwallet_webhook.php';
    return 'https://' . DOMAIN . '/telegram-bot/public/mini_app/api/' . $file;
}

function sw_return_url(string $hash = ''): string {
    $configured = trim((string)sw_setting('swapwallet_return_url', ''));
    if ($configured === '') $configured = 'https://' . DOMAIN . '/telegram-bot/public/mini_app/';
    return $hash ? rtrim($configured, '/') . '/' . $hash : $configured;
}

function sw_extract_result(array $json): array {
    $result = $json['result'] ?? $json['data'] ?? $json;
    return is_array($result) ? $result : [];
}

function sw_extract_invoice_id(array $json): ?string {
    $r = sw_extract_result($json);
    foreach (['id', 'invoiceId', 'supportCode', 'support_code'] as $key) {
        if (!empty($r[$key])) return (string)$r[$key];
    }
    return null;
}

function sw_extract_wallet_address(array $json): string {
    $r = sw_extract_result($json);
    if (!empty($r['walletAddress'])) return (string)$r['walletAddress'];
    if (!empty($r['wallet']['address'])) return (string)$r['wallet']['address'];
    return '';
}

function sw_extract_crypto_amount(array $json): array {
    $r = sw_extract_result($json);
    $amount = $r['amount'] ?? [];
    if (!is_array($amount)) $amount = [];

    // SwapWallet response may be either:
    // amount: { amount: { number, unit }, usdValue, userCurrencyValue }
    // or amount: { number, unit }
    $inner = $amount['amount'] ?? $amount;
    if (!is_array($inner)) $inner = [];

    return [
        'number' => (string)($inner['number'] ?? $inner['amount'] ?? ''),
        'unit' => (string)($inner['unit'] ?? $amount['unit'] ?? ''),
        'user_currency_number' => (string)($amount['userCurrencyValue']['number'] ?? ''),
        'user_currency_unit' => (string)($amount['userCurrencyValue']['unit'] ?? ''),
    ];
}

function sw_extract_payment_url(array $json): string {
    $r = sw_extract_result($json);
    foreach (['payment_url', 'paymentUrl', 'url', 'payUrl'] as $key) {
        if (!empty($r[$key]) && is_string($r[$key])) return $r[$key];
    }
    foreach (['paymentLinks', 'links', 'deepLinks'] as $key) {
        if (!empty($r[$key]) && is_array($r[$key])) {
            $first = '';
            foreach ($r[$key] as $link) {
                if (empty($link['url'])) continue;
                if ($first === '') $first = (string)$link['url'];
                $type = strtoupper((string)($link['type'] ?? $link['name'] ?? ''));
                if (in_array($type, ['WEBSITE', 'TELEGRAM_WEBAPP', 'TELEGRAM_BOT', 'SWAP_WALLET'], true)) {
                    return (string)$link['url'];
                }
            }
            if ($first !== '') return $first;
        }
    }
    return '';
}

function sw_status_from_json(array $json): string {
    $r = sw_extract_result($json);
    return strtoupper((string)($r['status'] ?? $r['invoiceStatus'] ?? $json['status'] ?? ''));
}

function sw_is_paid_status(?string $status): bool {
    return in_array(strtoupper(trim((string)$status)), ['PAID','SUCCESS','SUCCESSFUL','COMPLETED','CONFIRMED','SUCCEED','DONE'], true);
}

function sw_is_failed_status(?string $status): bool {
    return in_array(strtoupper(trim((string)$status)), ['FAILED','FAIL','CANCELED','CANCELLED','EXPIRED','REJECTED','TIMEOUT'], true);
}

function sw_error_message_from_response(array $resp): string {
    $http = (int)($resp['http'] ?? 0);
    $json = $resp['json'] ?? null;
    $body = (string)($resp['body'] ?? '');
    $curlError = (string)($resp['error'] ?? '');

    if ($curlError !== '') return 'خطای اتصال به SwapWallet: ' . $curlError;
    if ($http === 401 || $http === 403) return 'API Key سواپ‌ولت نامعتبر است یا دسترسی کافی ندارد. طبق مستندات، کلید apikey-... باید در هدر Authorization: Bearer ارسال شود. کلید را از پنل SwapWallet بررسی/تمدید کنید.';

    if (is_array($json)) {
        $parts = [];
        foreach (['message', 'error', 'errors', 'detail', 'details', 'violations'] as $key) {
            if (array_key_exists($key, $json)) $parts[] = sw_value_to_text($json[$key]);
        }
        if (!$parts) $parts[] = sw_value_to_text($json);
        return 'HTTP ' . $http . ' - ' . implode(' | ', array_filter($parts));
    }

    if ($body !== '') return 'HTTP ' . $http . ' - ' . mb_substr($body, 0, 1000);
    return 'پاسخ نامعتبر از SwapWallet. HTTP ' . $http;
}


function sw_http_request_public(string $method, string $url, ?array $payload = null): array {
    if (!function_exists('curl_init')) throw new RuntimeException('افزونه cURL روی هاست فعال نیست. از هاستینگ بخواهید cURL را فعال کند.');
    $headers = ['Accept: application/json'];
    $ch = curl_init($url);
    $opts = [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST => strtoupper($method),
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_CONNECTTIMEOUT => 15,
        CURLOPT_TIMEOUT => 45,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ];
    if ($payload !== null) {
        $headers[] = 'Content-Type: application/json';
        $opts[CURLOPT_HTTPHEADER] = $headers;
        $opts[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
    curl_setopt_array($ch, $opts);
    $body = curl_exec($ch);
    $err = curl_error($ch);
    $http = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    $json = json_decode((string)$body, true);
    return ['http' => $http, 'body' => (string)$body, 'json' => is_array($json) ? $json : null, 'error' => $err, 'auth_mode' => 'public'];
}

function sw_http_request(string $method, string $url, ?array $payload = null): array {
    $apiKey = sw_api_key();
    if ($apiKey === '') throw new RuntimeException('API Key سواپ‌ولت تنظیم نشده است. از پنل مدیریت مقدار swapwallet_api_key را وارد کنید.');
    // SwapWallet has two documented auth/key families:
    // - business-... for Merchant API: /v1/merchants/...
    // - apikey-... for Legacy Payment API: /v2/payment/{username}/...
    if (strpos($apiKey, 'apikey-') !== 0 && strpos($apiKey, 'business-') !== 0) {
        throw new RuntimeException('فرمت API Key سواپ‌ولت نامعتبر است. کلید باید با apikey- یا business- شروع شود.');
    }
    if (!function_exists('curl_init')) throw new RuntimeException('افزونه cURL روی هاست فعال نیست. از هاستینگ بخواهید cURL را فعال کند.');

    $headers = ['Accept: application/json', 'Authorization: Bearer ' . $apiKey];
    $ch = curl_init($url);
    $opts = [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST => strtoupper($method),
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_CONNECTTIMEOUT => 15,
        CURLOPT_TIMEOUT => 45,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ];
    if ($payload !== null) {
        $headers[] = 'Content-Type: application/json';
        $opts[CURLOPT_HTTPHEADER] = $headers;
        $opts[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
    curl_setopt_array($ch, $opts);
    $body = curl_exec($ch);
    $err = curl_error($ch);
    $http = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    $json = json_decode((string)$body, true);
    return ['http' => $http, 'body' => (string)$body, 'json' => is_array($json) ? $json : null, 'error' => $err];
}


function sw_auth_check(): array {
    $resp = sw_http_request('GET', sw_base_url() . '/v1/utils/authentication/check', null);
    return $resp;
}

function sw_invoice_endpoint_candidates(): array {
    $base = sw_base_url();
    $configured = trim((string)sw_setting('swapwallet_invoice_endpoint', ''));
    $username = sw_payment_username();
    if ($configured !== '' && $username !== '' && strpos($configured, '{username}') !== false) {
        $configured = str_replace('{username}', rawurlencode($username), $configured);
    }

    // SwapWallet currently documents the Business/Merchant API endpoint for business-... keys.
    // The user's key family can be apikey-..., so we also try the v2 Application/Payment routes.
    // For apikey-... we avoid putting a configured /v1/merchants endpoint first because it often
    // returns an opaque HTTP 500 before older routes are tried.
    $merchant = [
        $base . '/v1/merchants/invoices/temporary-wallet',
    ];
    $v2 = [];
    if ($username !== '') {
        // For apikey- keys, the most compatible fixed-IRT payment method in the legacy docs
        // is the receipt endpoint. It returns payment links and works well for wallet top-ups/orders.
        $v2[] = $base . '/v1/payment/' . rawurlencode($username) . '/resid';
        // Temporary wallet endpoint for direct wallet address payments.
        $v2[] = $base . '/v2/payment/' . rawurlencode($username) . '/invoices/temporary-wallet';
        // Advanced invoice fallback with allowedTokens/payment links.
        $v2[] = $base . '/v2/payment/' . rawurlencode($username) . '/invoices';
    }

    $candidates = [];
    if (sw_key_mode() === 'business') {
        if ($configured !== '') $candidates[] = $configured;
        $candidates = array_merge($candidates, $merchant, $v2);
    } else {
        // apikey-... or unknown: try v2 variants first, then configured/merchant as fallback.
        $candidates = array_merge($candidates, $v2);
        if ($configured !== '') $candidates[] = $configured;
        $candidates = array_merge($candidates, $merchant);
    }
    return array_values(array_unique(array_filter($candidates)));
}

function sw_invoice_payload_variants(array $payload): array {
    $ttl = max(300, min(21600, (int)($payload['ttl'] ?? sw_setting('swapwallet_ttl', '3600'))));
    $payload['ttl'] = $ttl;
    if (isset($payload['underPaidCoveragePercent'])) {
        $payload['underPaidCoveragePercent'] = (string)$payload['underPaidCoveragePercent'];
    }

    $token = (string)($payload['allowedToken'] ?? sw_default_token());
    $network = (string)($payload['network'] ?? sw_default_network());
    $v2 = $payload;
    $v2['allowedTokens'] = [['token' => $token, 'network' => $network]];
    $v2['feePayer'] = 'APPLICATION';
    unset($v2['allowedToken']);

    // Some deployments accept only the temporary-wallet shape, others accept the v2 invoice shape.
    return [$payload, $v2];
}


function sw_is_legacy_payment_endpoint(string $url): bool {
    return strpos($url, '/v2/payment/') !== false || strpos($url, '/v1/payment/') !== false;
}

function sw_endpoint_variant_count(string $url): int {
    if (preg_match('#/v1/payment/[^/]+/resid$#', $url)) return 3;
    if (strpos($url, '/temporary-wallet') !== false && strpos($url, '/v2/payment/') !== false) return 3;
    if (preg_match('#/v2/payment/[^/]+/invoices$#', $url)) return 2;
    return 2;
}

function sw_normalize_money_amount(int $amount): array {
    return ['number' => (string)$amount, 'unit' => sw_amount_unit()];
}

function sw_payload_for_endpoint(array $payload, string $url, int $variantIndex): array {
    $token = (string)($payload['allowedToken'] ?? sw_default_token());
    $network = (string)($payload['network'] ?? sw_default_network());
    $ttl = max(300, min(21600, (int)($payload['ttl'] ?? sw_setting('swapwallet_ttl', '3600'))));
    $underPaid = (string)($payload['underPaidCoveragePercent'] ?? '0');

    // Legacy fixed-IRT receipt endpoint: /v1/payment/{username}/resid.
    // This is often the most reliable apikey- endpoint for selling services priced in تومان.
    if (strpos($url, '/v1/payment/') !== false && preg_match('#/v1/payment/[^/]+/resid$#', $url)) {
        // The legacy resid endpoint is documented with only amount + ttl as required.
        // Some SwapWallet accounts return HTTP 500 when optional callback/metadata fields are sent.
        // Try progressively richer bodies so the first valid minimal request can succeed.
        if ($variantIndex === 0) {
            return [
                'amount' => $payload['amount'],
                'ttl' => $ttl,
            ];
        }
        if ($variantIndex === 1) {
            return array_filter([
                'amount' => $payload['amount'],
                'ttl' => $ttl,
                'userLanguage' => 'FA',
                'orderId' => (string)($payload['orderId'] ?? ''),
                'userId' => (string)($payload['userId'] ?? ''),
            ], static function($v) { return $v !== '' && $v !== null; });
        }
        return array_filter([
            'amount' => $payload['amount'],
            'ttl' => $ttl,
            'userLanguage' => 'FA',
            'webhookUrl' => (string)($payload['webhookUrl'] ?? ''),
            'returnUrl' => (string)($payload['returnUrl'] ?? ''),
            'orderId' => (string)($payload['orderId'] ?? ''),
            'description' => (string)($payload['description'] ?? ''),
            'customData' => (string)($payload['customData'] ?? ''),
            'userId' => (string)($payload['userId'] ?? ''),
        ], static function($v) { return $v !== '' && $v !== null; });
    }

    // Legacy apikey- endpoint is documented at /v2/payment/{username}/invoices/temporary-wallet.
    // It validates the request body strictly and rejects Merchant-only fields such as
    // userTelegramId, userDisplayName, returnUrl, feePayer, allowedTokens, etc.
    if (strpos($url, '/temporary-wallet') !== false && strpos($url, '/v2/payment/') !== false) {
        // Official required fields are amount + network + ttl. allowedToken is optional in the schema.
        if ($variantIndex === 0) {
            return [
                'amount' => $payload['amount'],
                'network' => $network,
                'ttl' => $ttl,
            ];
        }
        if ($variantIndex === 1) {
            return array_filter([
                'amount' => $payload['amount'],
                'network' => $network,
                'ttl' => $ttl,
                'orderId' => (string)($payload['orderId'] ?? ''),
                'webhookUrl' => (string)($payload['webhookUrl'] ?? ''),
                'userId' => (string)($payload['userId'] ?? ''),
            ], static function($v) { return $v !== '' && $v !== null; });
        }
        return array_filter([
            'amount' => $payload['amount'],
            'allowedToken' => $token,
            'network' => $network,
            'ttl' => $ttl,
            'underPaidCoveragePercent' => ($underPaid === '0' || $underPaid === '0.0') ? '' : $underPaid,
            'orderId' => (string)($payload['orderId'] ?? ''),
            'webhookUrl' => (string)($payload['webhookUrl'] ?? ''),
            'customData' => (string)($payload['customData'] ?? ''),
            'description' => (string)($payload['description'] ?? ''),
            'userId' => (string)($payload['userId'] ?? ''),
        ], static function($v) { return $v !== '' && $v !== null; });
    }

    // Legacy advanced invoice endpoint uses allowedTokens rather than allowedToken.
    if (sw_is_legacy_payment_endpoint($url) && preg_match('#/v2/payment/[^/]+/invoices$#', $url)) {
        return array_filter([
            'amount' => $payload['amount'],
            'allowedTokens' => [['token' => $token, 'network' => $network]],
            'feePayer' => 'APPLICATION',
            'userLanguage' => 'FA',
            'ttl' => $ttl,
            'underPaidCoveragePercent' => $underPaid,
            'returnUrl' => (string)($payload['returnUrl'] ?? ''),
            'webhookUrl' => (string)($payload['webhookUrl'] ?? ''),
            'orderId' => (string)($payload['orderId'] ?? ''),
            'description' => (string)($payload['description'] ?? ''),
            'customData' => (string)($payload['customData'] ?? ''),
            'userId' => (string)($payload['userId'] ?? ''),
        ], static function($v) { return $v !== '' && $v !== null; });
    }

    // Merchant/business endpoint supports richer user fields.
    $body = $payload;
    $body['ttl'] = $ttl;
    $body['underPaidCoveragePercent'] = $underPaid;
    if ($variantIndex === 1) {
        $body['allowedTokens'] = [['token' => $token, 'network' => $network]];
        $body['feePayer'] = 'APPLICATION';
        unset($body['allowedToken']);
    }
    return $body;
}


function sw_debug_log_path(): string {
    // __DIR__ = telegram-bot/public/mini_app/api; project root is three levels up.
    return dirname(__DIR__, 3) . '/storage/logs/swapwallet_last_error.json';
}

function sw_write_debug_log(array $data): void {
    $path = sw_debug_log_path();
    $dir = dirname($path);
    if (!is_dir($dir)) @mkdir($dir, 0755, true);
    $data['debug_path'] = $path;
    $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
    $ok = @file_put_contents($path, $json ?: '{}');
    if ($ok === false) {
        error_log('[swapwallet debug write failed] ' . $path . ' | ' . json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        $fallback = sys_get_temp_dir() . '/swapwallet_last_error.json';
        @file_put_contents($fallback, $json ?: '{}');
    }
}

function sw_safe_payload_for_log(array $payload): array {
    $copy = $payload;
    foreach (['apiKey','authorization','Authorization'] as $k) unset($copy[$k]);
    return $copy;
}

function sw_create_invoice(array $payload): array {
    $last = null;
    $attempts = [];
    foreach (sw_invoice_endpoint_candidates() as $url) {
        $variantCount = sw_endpoint_variant_count($url);
        for ($variantIndex = 0; $variantIndex < $variantCount; $variantIndex++) {
            $body = sw_payload_for_endpoint($payload, $url, $variantIndex);
            $isResidEndpoint = (strpos($url, '/v1/payment/') !== false && preg_match('#/v1/payment/[^/]+/resid$#', $url));
            // The current legacy spec lists /v1/payment/{username}/resid without the Authorization parameter,
            // while other invoice endpoints explicitly include AuthorizationHeader. Some shops return HTTP 500
            // when Authorization is sent to resid, so we try the public resid request first and then the
            // authenticated request as fallback.
            if ($isResidEndpoint) {
                $resp = sw_http_request_public('POST', $url, $body);
                if (!($resp['http'] >= 200 && $resp['http'] < 300 && is_array($resp['json']))) {
                    $publicResp = $resp;
                    $resp = sw_http_request('POST', $url, $body);
                    $resp['public_attempt'] = ['http'=>(int)($publicResp['http'] ?? 0), 'body'=>mb_substr((string)($publicResp['body'] ?? ''), 0, 500), 'error'=>(string)($publicResp['error'] ?? '')];
                }
            } else {
                $resp = sw_http_request('POST', $url, $body);
            }
            $resp['endpoint'] = $url;
            $resp['request_payload'] = $body;
            $last = $resp;
            $attempts[] = [
                'endpoint'=>$url,
                'http'=>(int)($resp['http'] ?? 0),
                'payload'=>sw_safe_payload_for_log($body),
                'body'=>mb_substr((string)($resp['body'] ?? ''), 0, 500),
                'auth_mode'=> (string)($resp['auth_mode'] ?? 'bearer'),
                'public_attempt'=> $resp['public_attempt'] ?? null
            ];
            if ($resp['http'] >= 200 && $resp['http'] < 300 && is_array($resp['json'])) {
                $resp['payload_variant'] = 'variant-' . $variantIndex;
                return $resp;
            }
            if (!in_array((int)$resp['http'], [400, 404, 405, 409, 422, 500, 502, 503], true)) {
                break 2;
            }
        }
    }
    // Save a safe diagnostic file on the server so HTTP 400 can be debugged without exposing the API key.
    sw_write_debug_log([
        'time' => date('c'),
        'key_mode' => sw_key_mode(),
        'base_url' => sw_base_url(),
        'payment_username' => sw_payment_username(),
        'default_token' => sw_default_token(),
        'default_network' => sw_default_network(),
        'amount_unit' => sw_amount_unit(),
        'attempts' => $attempts,
    ]);
    $message = sw_error_message_from_response($last ?: ['http'=>0,'body'=>'','json'=>null,'error'=>'unknown']);
    if (sw_key_mode() === 'legacy') {
        $message .= "\n\nراهنما: این نسخه برای کلید apikey- ابتدا endpoint رسید ثابت IRT یعنی /v1/payment/{username}/resid را تست می‌کند و سپس سراغ فاکتورهای v2 می‌رود. فایل تشخیصی در storage/logs/swapwallet_last_error.json ساخته می‌شود.";
    }
    throw new RuntimeException($message);
}

function sw_get_invoice_by_order_id(string $orderId): array {
    $urls = [sw_base_url() . '/v1/merchants/invoices/by-order-id/' . rawurlencode($orderId)];
    $username = sw_payment_username();
    if ($username !== '') $urls[] = sw_base_url() . '/v2/payment/' . rawurlencode($username) . '/invoices/with-order-id/' . rawurlencode($orderId);
    $last = null;
    foreach ($urls as $url) {
        $resp = sw_http_request('GET', $url, null);
        $last = $resp;
        if ($resp['http'] >= 200 && $resp['http'] < 300 && is_array($resp['json'])) return $resp;
        if (!in_array((int)$resp['http'], [404, 500, 502, 503], true)) break;
    }
    throw new RuntimeException('استعلام فاکتور ناموفق: ' . sw_error_message_from_response($last ?: ['http'=>0,'body'=>'','json'=>null,'error'=>'unknown']));
}

function sw_make_order_invoice_payload(array $order, array $plan, int $amount, int $userId): array {
    $providerOrderId = 'ORDER-' . (int)$order['id'];
    return [
        'amount' => ['number' => (string)$amount, 'unit' => sw_amount_unit()],
        'allowedToken' => sw_default_token(),
        'network' => sw_default_network(),
        'webhookUrl' => sw_webhook_url('order'),
        'returnUrl' => sw_return_url('#orders'),
        'ttl' => max(60, (int)sw_setting('swapwallet_ttl', '86400')),
        'underPaidCoveragePercent' => max(0, (int)sw_setting('swapwallet_underpaid_percent', '0')),
        'orderId' => $providerOrderId,
        'customData' => json_encode(['type' => 'order', 'order_id' => (int)$order['id'], 'provider_order_id' => $providerOrderId], JSON_UNESCAPED_UNICODE),
        'description' => 'خرید سرویس OpenVPN | L2TP - سفارش #' . (int)$order['id'],
        'userId' => (string)$userId,
        'userTelegramId' => $userId,
        'userDisplayName' => 'Telegram User ' . $userId,
        'userLanguage' => 'FA',
    ];
}

function sw_make_wallet_invoice_payload(array $tx, int $amount, int $userId): array {
    $providerOrderId = 'WALLET-' . (int)$tx['id'];
    return [
        'amount' => ['number' => (string)$amount, 'unit' => sw_amount_unit()],
        'allowedToken' => sw_default_token(),
        'network' => sw_default_network(),
        'webhookUrl' => sw_webhook_url('wallet'),
        'returnUrl' => sw_return_url('#wallet'),
        'ttl' => max(60, (int)sw_setting('swapwallet_ttl', '86400')),
        'underPaidCoveragePercent' => max(0, (int)sw_setting('swapwallet_underpaid_percent', '0')),
        'orderId' => $providerOrderId,
        'customData' => json_encode(['type' => 'wallet_topup', 'wallet_transaction_id' => (int)$tx['id'], 'provider_order_id' => $providerOrderId], JSON_UNESCAPED_UNICODE),
        'description' => 'افزایش موجودی کیف پول #' . (int)$tx['id'],
        'userId' => (string)$userId,
        'userTelegramId' => $userId,
        'userDisplayName' => 'Telegram User ' . $userId,
        'userLanguage' => 'FA',
    ];
}

function sw_order_id_from_webhook(array $input): string {
    foreach (['orderId','order_id','merchant_order_id','externalOrderId'] as $key) {
        if (!empty($input[$key])) return (string)$input[$key];
    }
    if (!empty($input['customData'])) {
        $custom = is_array($input['customData']) ? $input['customData'] : json_decode((string)$input['customData'], true);
        if (is_array($custom) && !empty($custom['provider_order_id'])) return (string)$custom['provider_order_id'];
    }
    return '';
}

function sw_json_ok(array $data = []): void {
    echo json_encode(['ok' => true] + $data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

function sw_json_error(string $error, array $extra = []): void {
    http_response_code(200);
    echo json_encode(['ok' => false, 'error' => $error] + $extra, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}
