<?php
/**
 * scan_crypto_deposits.php (FINAL - cPanel-safe + NetworkCode-based + 3 specialized tables)
 *
 * ✅ Multi-asset EVM (Native + Tokens) حسب crypto_asset_networks
 * ✅ عنوان واحد لكل user + network_code عبر JOIN على network_code
 * ✅ State في user_deposit_address_state (last_chain_balance_wei + pending_wei)
 *
 * ✅ الآن السجلات مفصولة إلى 3 جداول:
 * 1) crypto_deposit_detections:
 *    - detected   = تم اكتشاف زيادة
 *    - below_min  = زيادة أقل من min_deposit
 *
 * 2) crypto_sweep_operations:
 *    - sweep = تم إرسال معاملة سحب من عنوان الإيداع إلى محفظة التجميع
 *    - request_status: pending | completed | canceled
 *    - مرتبط بـ ledger_entries عبر ref_type='crypto_sweep' و ref_id = sweep_id
 *
 * 3) gas_wallet_operations:
 *    - gas_topup  = شحن غاز من محفظة الغاز إلى عنوان الإيداع
 *    - gas_refund = إرجاع فائض الـ Native من عنوان الإيداع إلى محفظة الغاز
 *    - request_status: pending | completed | canceled
 *    - مع حفظ الرصيد المتبقي في محفظة الغاز بعد العملية (حسب قراءة السلسلة)
 *
 * ✅ FIX مهم:
 * - بعد تأكيد TX (سواء finalize أو confirm loop) نقوم بتحديث last_chain_balance_wei
 *   بطرح sweep/credit wei (مع عدم النزول تحت الصفر)
 *
 * ✅ تحسين:
 * - إذا يوجد sweep pending سابق لنفس uda+asset+network: نسمح بالكشف، لكن نمنع sweep حتى يتأكد السابق.
 */

declare(strict_types=1);

error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
ini_set('display_errors', '0');

/* ================== I/O (no noisy output) ================== */
function is_cli(): bool { return PHP_SAPI === 'cli'; }
function out(string $s): void { if (is_cli()) echo $s; }
if (!is_cli()) header('Content-Type: text/plain; charset=UTF-8');

/* ================== Requirements ================== */
if (!function_exists('curl_init')) { out("cURL غير مفعّل.\n"); exit; }
if (!function_exists('bcadd'))     { out("BCMath غير مفعّل.\n"); exit; }
if (!function_exists('gmp_init'))  { out("GMP غير مفعّل.\n"); exit; }

/* ================== Autoload + Config (paths safe) ================== */
$autoloadCandidates = [ __DIR__ . '/vendor/autoload.php', __DIR__ . '/../vendor/autoload.php' ];
$autoloadPath = null;
foreach ($autoloadCandidates as $p) { if (is_readable($p)) { $autoloadPath = $p; break; } }
if (!$autoloadPath) { out("vendor/autoload.php غير موجود.\n"); exit; }
require $autoloadPath;

$configCandidates = [ __DIR__ . '/config.php', __DIR__ . '/../config.php' ];
$configPath = null;
foreach ($configCandidates as $p) { if (is_readable($p)) { $configPath = $p; break; } }
if (!$configPath) { out("config.php غير موجود.\n"); exit; }
require $configPath;

/* ================== PDO handle ================== */
$pdo = null;
if (isset($conn) && ($conn instanceof PDO)) $pdo = $conn;
elseif (isset($pdo) && ($pdo instanceof PDO)) $pdo = $pdo;

if (!($pdo instanceof PDO)) { out("PDO غير متصل.\n"); exit; }
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

/* ================== Logging ================== */
function yc_scan_log(string $msg): void {
    $dir = __DIR__ . '/logs';
    if (!is_dir($dir)) { @mkdir($dir, 0755, true); }
    $file = $dir . '/yc_scan_deposits.log';
    @error_log('['.date('Y-m-d H:i:s').'] '.$msg.PHP_EOL, 3, $file);
}

/* ================== Defaults ================== */
define('DEFAULT_RPC_ENDPOINT', 'https://bsc-dataseed.binance.org/');
define('DEFAULT_CHAIN_ID', 56);

define('DEFAULT_GAS_LIMIT_NATIVE', 21000);
define('DEFAULT_GAS_LIMIT_TOKEN', 100000);

define('DEFAULT_GAS_TOPUP_NATIVE', '0.0001');

// Refund buffer (20%)
define('GAS_REFUND_BUFFER_DIV', '5');
define('DEFAULT_MIN_GAS_REFUND_NATIVE', '0');

define('LEDGER_REF_TYPE_SWEEP', 'crypto_sweep');

/* ================== wallet.env loader (cPanel-safe) ================== */
function yc_detect_home_dir(): string {
    $candidates = [];
    $envHome = getenv('HOME');
    if (is_string($envHome) && $envHome !== '') $candidates[] = $envHome;

    $srvHome = $_SERVER['HOME'] ?? '';
    if (is_string($srvHome) && $srvHome !== '') $candidates[] = $srvHome;

    $dir = __DIR__;
    if (preg_match('~^(/home/[^/]+)~', $dir, $m)) $candidates[] = $m[1];

    $candidates[] = dirname(__DIR__, 3);

    foreach ($candidates as $h) {
        $h = rtrim((string)$h, '/');
        if ($h !== '' && is_dir($h)) return $h;
    }
    return '/home';
}

function yc_load_wallet_env(): array {
    static $cfg = null;
    if ($cfg !== null) return $cfg;

    $home = yc_detect_home_dir();
    $path = rtrim($home, '/') . '/.secure_env/wallet.env';

    if (!is_readable($path)) {
        $fallback = __DIR__ . '/wallet.env';
        if (is_readable($fallback)) $path = $fallback;
    }

    if (!is_readable($path)) {
        throw new RuntimeException('wallet.env غير موجود/غير قابل للقراءة في: ' . $path);
    }

    $cfg = parse_ini_file($path, false, INI_SCANNER_RAW);
    if (!is_array($cfg)) throw new RuntimeException('فشل في قراءة wallet.env');
    return $cfg;
}

/* ================== Load wallet.env ================== */
try {
    $env = yc_load_wallet_env();
} catch (Throwable $e) {
    yc_scan_log("ENV_ERROR: ".$e->getMessage());
    out($e->getMessage()."\n");
    exit;
}

$mnemonic = trim((string)($env['WALLET_MNEMONIC'] ?? ''));
if ($mnemonic === '') { out("WALLET_MNEMONIC فارغ.\n"); exit; }

$collectAddress = strtolower(trim((string)($env['WALLET_COLLECT_ADDRESS'] ?? '')));

$gasAddrEnv  = strtolower(trim((string)($env['GAS_WALLET_ADDRESS'] ?? '')));
$gasPkEnv    = trim((string)($env['GAS_WALLET_PRIVATE_KEY'] ?? ''));

$envTopupStr = trim((string)($env['GAS_TOPUP_NATIVE'] ?? ($env['GAS_TOPUP_BNB'] ?? '')));
if ($envTopupStr === '' || !preg_match('/^\d+(\.\d+)?$/', $envTopupStr)) {
    $envTopupStr = DEFAULT_GAS_TOPUP_NATIVE;
}

$envRefundMinStr = trim((string)($env['GAS_REFUND_MIN_NATIVE'] ?? ''));
if ($envRefundMinStr === '' || !preg_match('/^\d+(\.\d+)?$/', $envRefundMinStr)) {
    $envRefundMinStr = DEFAULT_MIN_GAS_REFUND_NATIVE;
}

$gasDerivationIndex = (int)($env['GAS_WALLET_DERIVATION_INDEX'] ?? 2);

/* ================== Web3 libs ================== */
use Web3p\EthereumTx\Transaction;
use kornrunner\Keccak;
use Elliptic\EC;

/* ================== RPC ================== */
function rpc(string $rpcEndpoint, string $method, array $params = []): ?array {
    $payload = json_encode(["jsonrpc"=>"2.0","id"=>1,"method"=>$method,"params"=>$params], JSON_UNESCAPED_SLASHES);
    $ch = curl_init($rpcEndpoint);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $payload,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_TIMEOUT => 25,
    ]);
    $res = curl_exec($ch);
    if ($res === false) { curl_close($ch); return null; }
    curl_close($ch);
    $j = json_decode($res, true);
    return is_array($j) ? $j : null;
}

/* ================== Big numbers helpers ================== */
function hexToDecBig(string $hex): string {
    $hex = strtolower(trim($hex));
    if (str_starts_with($hex, '0x')) $hex = substr($hex, 2);
    if ($hex === '') return '0';

    $dec = '0';
    for ($i=0, $len=strlen($hex); $i<$len; $i++) {
        $dec = bcmul($dec, '16', 0);
        $dec = bcadd($dec, (string)hexdec($hex[$i]), 0);
    }
    return $dec;
}

function decToHexString(string $dec): string {
    $dec = ltrim($dec, '0');
    if ($dec === '') return '0';
    return gmp_strval(gmp_init($dec, 10), 16);
}

function decimalToWei(string $amount, int $decimals): string {
    $amount = trim($amount);
    if ($amount === '') return '0';
    $parts = explode('.', $amount, 2);
    $int = preg_replace('/\D/', '', $parts[0] ?? '0');
    $frac = preg_replace('/\D/', '', $parts[1] ?? '');
    $frac = substr($frac, 0, $decimals);
    $frac = str_pad($frac, $decimals, '0', STR_PAD_RIGHT);
    $wei = ltrim($int . $frac, '0');
    return $wei === '' ? '0' : $wei;
}

function weiToDecimal(string $wei, int $decimals, int $scale = 18): string {
    $base = bcpow('10', (string)$decimals, 0);
    return bcdiv($wei, $base, $scale);
}

function cleanPrivateKey(string $key): string {
    $key = strtolower(trim($key));
    if (str_starts_with($key, '0x')) $key = substr($key, 2);
    if (!preg_match('/^[0-9a-f]{64}$/', $key)) throw new RuntimeException("Private key غير صالح");
    return $key;
}

/* ================== ERC20 calldata ================== */
function buildBalanceOfData(string $address): string {
    $methodSig = '70a08231';
    $addr = strtolower($address);
    if (str_starts_with($addr, '0x')) $addr = substr($addr, 2);
    $padded = str_pad($addr, 64, '0', STR_PAD_LEFT);
    return '0x' . $methodSig . $padded;
}

function buildErc20TransferData(string $to, string $amountHexNo0x): string {
    $toNo0x = strtolower(substr($to, 2));
    $toPadded = str_pad($toNo0x, 64, '0', STR_PAD_LEFT);
    $amtPadded = str_pad($amountHexNo0x, 64, '0', STR_PAD_LEFT);
    return 'a9059cbb' . $toPadded . $amtPadded;
}

/* ================== HD derive (EVM) ================== */
function mnemonic_to_seed(string $mnemonic, string $passphrase = ""): string {
    $salt = "mnemonic" . $passphrase;
    return hash_pbkdf2("sha512", $mnemonic, $salt, 2048, 64, true);
}
function public_key_from_private(string $privKey): string {
    $ec = new EC('secp256k1');
    $pub = $ec->keyFromPrivate(bin2hex($privKey))->getPublic(false, "hex");
    return hex2bin($pub);
}
function pub_to_eth_address(string $pubKey): string {
    $pub = substr($pubKey, 1);
    $hash = Keccak::hash($pub, 256);
    return "0x" . strtolower(substr($hash, 24));
}
function derive_child_key(string $seed, string $path): string {
    $I = hash_hmac('sha512', $seed, "Bitcoin seed", true);
    $priv = substr($I, 0, 32);
    $chain = substr($I, 32);

    $segments = explode("/", $path);
    $n = gmp_init("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16);

    foreach ($segments as $seg) {
        if ($seg === "m" || $seg === "") continue;

        $isHardened = false;
        if (str_ends_with($seg, "'")) { $isHardened = true; $seg = (int)$seg + 0x80000000; }
        else { $seg = (int)$seg; }

        $index = pack("N", $seg);
        $data = $isHardened ? ("\x00" . $priv . $index) : (public_key_from_private($priv) . $index);

        $I = hash_hmac('sha512', $data, $chain, true);
        $Il = substr($I, 0, 32);
        $Ir = substr($I, 32);

        $child = gmp_mod(
            gmp_add(gmp_init(bin2hex($Il), 16), gmp_init(bin2hex($priv), 16)),
            $n
        );
        $priv = hex2bin(str_pad(gmp_strval($child, 16), 64, "0", STR_PAD_LEFT));
        $chain = $Ir;
    }
    return $priv;
}
function eth_wallet_from_index(string $mnemonic, int $index): array {
    $seed = mnemonic_to_seed($mnemonic);
    $path = "m/44'/60'/0'/0/{$index}";
    $priv = derive_child_key($seed, $path);
    $addr = pub_to_eth_address(public_key_from_private($priv));
    return ["private_key" => bin2hex($priv), "address" => $addr, "path" => $path, "index" => $index];
}

/* ================== Chain helpers ================== */
function getGasPriceHex(string $rpcEndpoint): ?string {
    $r = rpc($rpcEndpoint, 'eth_gasPrice');
    return ($r && isset($r['result'])) ? $r['result'] : null;
}
function getGasPriceWeiDec(string $rpcEndpoint): ?string {
    $hex = getGasPriceHex($rpcEndpoint);
    return $hex ? hexToDecBig($hex) : null;
}
function getNativeBalanceWeiDec(string $rpcEndpoint, string $address): string {
    $r = rpc($rpcEndpoint, 'eth_getBalance', [strtolower($address), 'latest']);
    if (!$r || !isset($r['result'])) return '0';
    return hexToDecBig($r['result']);
}
function getTokenBalanceWeiDec(string $rpcEndpoint, string $contract, string $address): ?string {
    $r = rpc($rpcEndpoint, 'eth_call', [[ 'to'=>strtolower($contract), 'data'=>buildBalanceOfData($address) ], 'latest']);
    if (!$r || !isset($r['result'])) return null;
    return hexToDecBig($r['result']);
}
function getTxReceiptStatus(string $rpcEndpoint, string $txHash): string {
    $r = rpc($rpcEndpoint, 'eth_getTransactionReceipt', [$txHash]);
    if (!$r || !array_key_exists('result', $r) || $r['result'] === null) return 'pending';

    $status = $r['result']['status'] ?? null;
    if ($status === null) return 'pending';

    $status = strtolower((string)$status);
    if ($status === '0x1' || $status === '0x01') return 'success';
    if ($status === '0x0' || $status === '0x00') return 'failed';
    return 'pending';
}

/* ================== Send transactions ================== */
function sendNativeTransferTx(
    string $rpcEndpoint,
    int $chainId,
    string $fromAddress,
    string $fromPrivateKeyHex,
    string $toAddress,
    string $valueWeiDec,
    int $gasLimit
): ?string {
    $nonceRes = rpc($rpcEndpoint, 'eth_getTransactionCount', [strtolower($fromAddress), 'pending']);
    if (!$nonceRes || !isset($nonceRes['result'])) return null;
    $nonceDec = hexdec($nonceRes['result']);

    $gasPriceHex = getGasPriceHex($rpcEndpoint);
    if ($gasPriceHex === null) return null;

    $pk = cleanPrivateKey($fromPrivateKeyHex);
    $valueHex = decToHexString($valueWeiDec);

    $txData = [
        'nonce'    => '0x' . dechex($nonceDec),
        'gasPrice' => $gasPriceHex,
        'gasLimit' => '0x' . dechex($gasLimit),
        'to'       => strtolower($toAddress),
        'value'    => '0x' . $valueHex,
        'data'     => '',
        'chainId'  => $chainId,
    ];

    $tx = new Transaction($txData);
    $signed = '0x' . $tx->sign($pk);

    $sendRes = rpc($rpcEndpoint, 'eth_sendRawTransaction', [$signed]);
    if (!$sendRes || isset($sendRes['error'])) return null;
    return $sendRes['result'] ?? null;
}

function sendTokenTransferTx(
    string $rpcEndpoint,
    int $chainId,
    string $fromAddress,
    string $fromPrivateKeyHex,
    string $tokenContract,
    string $toAddress,
    string $amountWeiDec,
    int $gasLimit
): ?string {
    $nonceRes = rpc($rpcEndpoint, 'eth_getTransactionCount', [strtolower($fromAddress), 'pending']);
    if (!$nonceRes || !isset($nonceRes['result'])) return null;
    $nonceDec = hexdec($nonceRes['result']);

    $gasPriceHex = getGasPriceHex($rpcEndpoint);
    if ($gasPriceHex === null) return null;

    $pk = cleanPrivateKey($fromPrivateKeyHex);
    $amtHex = decToHexString($amountWeiDec);
    $data = buildErc20TransferData($toAddress, $amtHex);

    $txData = [
        'nonce'    => '0x' . dechex($nonceDec),
        'gasPrice' => $gasPriceHex,
        'gasLimit' => '0x' . dechex($gasLimit),
        'to'       => strtolower($tokenContract),
        'value'    => '0x0',
        'data'     => '0x' . $data,
        'chainId'  => $chainId,
    ];

    $tx = new Transaction($txData);
    $signed = '0x' . $tx->sign($pk);

    $sendRes = rpc($rpcEndpoint, 'eth_sendRawTransaction', [$signed]);
    if (!$sendRes || isset($sendRes['error'])) return null;
    return $sendRes['result'] ?? null;
}

/* ================== Gas Refund (excess native) ================== */
function reclaimExcessNativeToGasWallet(
    string $rpcEndpoint,
    int $chainId,
    string $fromAddress,
    string $fromPrivateKeyHex,
    string $gasWalletAddress,
    string $minRefundWeiDec = '0',
    int $gasLimit = DEFAULT_GAS_LIMIT_NATIVE
): ?array {
    $fromAddress = strtolower($fromAddress);
    $gasWalletAddress = strtolower($gasWalletAddress);
    if ($fromAddress === '' || $gasWalletAddress === '' || $fromAddress === $gasWalletAddress) return null;

    $balanceWei = getNativeBalanceWeiDec($rpcEndpoint, $fromAddress);
    if (bccomp($balanceWei, '0', 0) <= 0) return null;

    $gasPriceWei = getGasPriceWeiDec($rpcEndpoint);
    if ($gasPriceWei === null) return null;

    $feeWei = bcmul($gasPriceWei, (string)$gasLimit, 0);
    $bufferWei = bcdiv($feeWei, GAS_REFUND_BUFFER_DIV, 0);
    $keepWei = bcadd($feeWei, $bufferWei, 0);

    if (bccomp($balanceWei, $keepWei, 0) <= 0) return null;
    $sendWei = bcsub($balanceWei, $keepWei, 0);

    if (bccomp($minRefundWeiDec, '0', 0) > 0 && bccomp($sendWei, $minRefundWeiDec, 0) < 0) {
        return null;
    }

    $tx = sendNativeTransferTx(
        $rpcEndpoint,
        $chainId,
        $fromAddress,
        $fromPrivateKeyHex,
        $gasWalletAddress,
        $sendWei,
        $gasLimit
    );
    if (!$tx) return null;

    return [
        'tx_hash' => $tx,
        'send_wei' => $sendWei,
        'fee_wei' => $feeWei,
        'keep_wei' => $keepWei,
        'gas_price_wei' => $gasPriceWei,
        'balance_wei' => $balanceWei,
        'gas_limit' => (string)$gasLimit,
    ];
}

/* ================== DB schema helpers ================== */
function column_exists(PDO $db, string $table, string $col): bool {
    $st = $db->prepare("SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=:t AND COLUMN_NAME=:c LIMIT 1");
    $st->execute([':t'=>$table, ':c'=>$col]);
    return (bool)$st->fetchColumn();
}

function ensureStateRow(PDO $pdo, array $t): void {
    $stmt = $pdo->prepare("
        INSERT INTO user_deposit_address_state
          (uda_id, user_id, asset_id, network_id, network_code, address, last_chain_balance_wei, pending_wei, created_at, updated_at)
        VALUES
          (:uda, :uid, :aid, :nid, :ncode, :addr, '0', '0', NOW(), NOW())
        ON DUPLICATE KEY UPDATE
          updated_at = NOW()
    ");
    $stmt->execute([
        'uda'   => (int)$t['uda_id'],
        'uid'   => (int)$t['user_id'],
        'aid'   => (int)$t['asset_id'],
        'nid'   => (int)$t['network_id'],
        'ncode' => (string)$t['network_code'],
        'addr'  => (string)$t['address'],
    ]);
}

/* ================== Insert helpers (NEW tables) ================== */
function insertDetection(PDO $pdo, array $d): int {
    $st = $pdo->prepare("
        INSERT INTO crypto_deposit_detections
        (state_id, uda_id, user_id, asset_id, network_id, network_code, deposit_address,
         event_type,
         delta_wei, delta_amount,
         pending_after_wei, pending_after_amount,
         current_balance_wei, current_balance_amount,
         min_deposit, note, created_at)
        VALUES
        (:state_id, :uda_id, :user_id, :asset_id, :network_id, :network_code, :deposit_address,
         :event_type,
         :delta_wei, :delta_amount,
         :pending_after_wei, :pending_after_amount,
         :current_balance_wei, :current_balance_amount,
         :min_deposit, :note, NOW())
    ");
    $st->execute([
        'state_id' => $d['state_id'] ?? null,
        'uda_id' => $d['uda_id'],
        'user_id' => $d['user_id'],
        'asset_id' => $d['asset_id'],
        'network_id' => $d['network_id'],
        'network_code' => $d['network_code'],
        'deposit_address' => $d['deposit_address'],

        'event_type' => $d['event_type'],

        'delta_wei' => $d['delta_wei'] ?? null,
        'delta_amount' => $d['delta_amount'] ?? null,

        'pending_after_wei' => $d['pending_after_wei'] ?? null,
        'pending_after_amount' => $d['pending_after_amount'] ?? null,

        'current_balance_wei' => $d['current_balance_wei'] ?? null,
        'current_balance_amount' => $d['current_balance_amount'] ?? null,

        'min_deposit' => $d['min_deposit'] ?? null,
        'note' => $d['note'] ?? null,
    ]);
    return (int)$pdo->lastInsertId();
}

function insertSweep(PDO $pdo, array $d): int {
    $st = $pdo->prepare("
        INSERT INTO crypto_sweep_operations
        (state_id, uda_id, user_id, asset_id, network_id, network_code,
         deposit_address, collect_address,
         amount_wei, amount_decimal,
         tx_hash, request_status,
         ledger_ref_type, ledger_ref_id,
         note, created_at)
        VALUES
        (:state_id, :uda_id, :user_id, :asset_id, :network_id, :network_code,
         :deposit_address, :collect_address,
         :amount_wei, :amount_decimal,
         :tx_hash, :request_status,
         :ledger_ref_type, :ledger_ref_id,
         :note, NOW())
    ");
    $st->execute([
        'state_id' => $d['state_id'] ?? null,
        'uda_id' => $d['uda_id'],
        'user_id' => $d['user_id'],
        'asset_id' => $d['asset_id'],
        'network_id' => $d['network_id'],
        'network_code' => $d['network_code'],
        'deposit_address' => $d['deposit_address'],
        'collect_address' => $d['collect_address'],
        'amount_wei' => $d['amount_wei'],
        'amount_decimal' => $d['amount_decimal'],
        'tx_hash' => $d['tx_hash'] ?? null,
        'request_status' => $d['request_status'] ?? 'pending',
        'ledger_ref_type' => $d['ledger_ref_type'] ?? null,
        'ledger_ref_id' => $d['ledger_ref_id'] ?? null,
        'note' => $d['note'] ?? null,
    ]);
    return (int)$pdo->lastInsertId();
}

function updateSweepStatus(PDO $pdo, int $sweepId, string $status, ?string $note = null, bool $setConfirmed = false): void {
    $sql = "UPDATE crypto_sweep_operations
            SET request_status=:st,
                note=COALESCE(:note, note),
                updated_at=NOW()";
    if ($setConfirmed) $sql .= ", confirmed_at=NOW()";
    $sql .= " WHERE id=:id";
    $pdo->prepare($sql)->execute(['st'=>$status,'note'=>$note,'id'=>$sweepId]);
}

function insertGasOp(PDO $pdo, array $d): int {
    $st = $pdo->prepare("
        INSERT INTO gas_wallet_operations
        (network_id, network_code, chain_id,
         gas_wallet_address,
         op_type, direction,
         from_address, to_address,
         amount_wei, amount_native,
         tx_hash, request_status,
         gas_wallet_balance_after_wei, gas_wallet_balance_after_native,
         note, created_at)
        VALUES
        (:network_id, :network_code, :chain_id,
         :gas_wallet_address,
         :op_type, :direction,
         :from_address, :to_address,
         :amount_wei, :amount_native,
         :tx_hash, :request_status,
         :bal_after_wei, :bal_after_native,
         :note, NOW())
    ");
    $st->execute([
        'network_id' => $d['network_id'],
        'network_code' => $d['network_code'],
        'chain_id' => $d['chain_id'],
        'gas_wallet_address' => $d['gas_wallet_address'],
        'op_type' => $d['op_type'],
        'direction' => $d['direction'],
        'from_address' => $d['from_address'],
        'to_address' => $d['to_address'],
        'amount_wei' => $d['amount_wei'],
        'amount_native' => $d['amount_native'],
        'tx_hash' => $d['tx_hash'] ?? null,
        'request_status' => $d['request_status'] ?? 'pending',
        'bal_after_wei' => $d['gas_wallet_balance_after_wei'] ?? null,
        'bal_after_native' => $d['gas_wallet_balance_after_native'] ?? null,
        'note' => $d['note'] ?? null,
    ]);
    return (int)$pdo->lastInsertId();
}

function updateGasOpStatus(PDO $pdo, int $opId, string $status, ?string $note = null, bool $setConfirmed = false): void {
    $sql = "UPDATE gas_wallet_operations
            SET request_status=:st,
                note=COALESCE(:note, note),
                updated_at=NOW()";
    if ($setConfirmed) $sql .= ", confirmed_at=NOW()";
    $sql .= " WHERE id=:id";
    $pdo->prepare($sql)->execute(['st'=>$status,'note'=>$note,'id'=>$opId]);
}

/* ================== Ledger helpers ================== */
function insertLedger(PDO $pdo, string $note, string $status, int $refId, string $refType, string $balanceType, string $amount, int $assetId, int $userId): void {
    $stmt = $pdo->prepare("
        INSERT INTO ledger_entries (created_at, note, status, ref_id, ref_type, balance_type, amount, asset_id, user_id)
        VALUES (NOW(), :note, :status, :ref_id, :ref_type, :balance_type, :amount, :asset_id, :user_id)
    ");
    $stmt->execute([
        'note'=>$note,
        'status'=>$status,
        'ref_id'=>$refId,
        'ref_type'=>$refType,
        'balance_type'=>$balanceType,
        'amount'=>$amount,
        'asset_id'=>$assetId,
        'user_id'=>$userId,
    ]);
}

function updateLedgerStatus(PDO $pdo, int $refId, string $refType, string $newStatus): void {
    $stmt = $pdo->prepare("UPDATE ledger_entries SET status = :st WHERE ref_type = :rt AND ref_id = :rid");
    $stmt->execute(['st'=>$newStatus,'rt'=>$refType,'rid'=>$refId]);
}

/* ================== Gas wallet + Collect wallet ================== */
if ($gasAddrEnv !== '' && $gasPkEnv !== '') {
    $gasAddress = $gasAddrEnv;
    $gasPrivKey = $gasPkEnv;
} else {
    $gasWallet  = eth_wallet_from_index($mnemonic, $gasDerivationIndex);
    $gasAddress = strtolower($gasWallet['address']);
    $gasPrivKey = $gasWallet['private_key'];
}
if ($collectAddress === '') $collectAddress = $gasAddress;

/* ================== Detect optional column always_topup_gas ================== */
$hasAlwaysTopupCol = column_exists($pdo, 'crypto_asset_networks', 'always_topup_gas');

/* ================== Targets (JOIN by network_code) ================== */
$sql = "
SELECT
  uda.id AS uda_id,
  uda.user_id,
  LOWER(uda.address) AS address,
  uda.derivation_index,
  uda.network_code AS uda_network_code,

  can.id AS network_id,
  can.network_code,
  can.min_deposit,

  can.contract_address,
  can.decimals_onchain,
  can.is_native,
  can.rpc_endpoint,
  can.chain_id,
  can.gas_limit,
  can.gas_topup_native
  ".($hasAlwaysTopupCol ? ", can.always_topup_gas" : "").",

  a.id AS asset_id,
  a.code AS asset_code,
  a.type AS asset_type
FROM user_deposit_addresses uda
JOIN crypto_asset_networks can
  ON can.network_code = uda.network_code
 AND can.is_active = 1
JOIN assets a
  ON a.id = can.asset_id
WHERE uda.is_active = 1
  AND a.type = 'crypto'
  AND (
    can.is_native = 1
    OR (can.is_native = 0 AND can.contract_address IS NOT NULL AND can.contract_address <> '')
  )
ORDER BY uda.id ASC, can.id ASC
";

$targets = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
if (!$targets) { out("لا توجد عناوين نشطة/صالحة للمعالجة.\n"); exit; }

/* =========================================================
   finalize pending sweeps (per uda+asset+network)
   ========================================================= */
function finalizePendingSweepIfAny(
    PDO $pdo,
    array $t,
    string $rpcEndpoint,
    string $mnemonic,
    int $chainId,
    string $gasAddress,
    string $minRefundWeiDec
): string {
    $udaId = (int)$t['uda_id'];
    $assetId = (int)$t['asset_id'];
    $networkId = (int)$t['network_id'];

    $q = $pdo->prepare("
        SELECT id, tx_hash, amount_wei, deposit_address
        FROM crypto_sweep_operations
        WHERE uda_id=:uda AND asset_id=:aid AND network_id=:nid
          AND request_status='pending'
          AND tx_hash IS NOT NULL
        ORDER BY id DESC
        LIMIT 1
    ");
    $q->execute(['uda'=>$udaId,'aid'=>$assetId,'nid'=>$networkId]);
    $row = $q->fetch(PDO::FETCH_ASSOC);
    if (!$row) return 'none';

    $sweepId = (int)$row['id'];
    $txHash  = (string)$row['tx_hash'];
    $amtWei  = (string)$row['amount_wei'];
    $depAddr = strtolower((string)$row['deposit_address']);

    $st = getTxReceiptStatus($rpcEndpoint, $txHash);
    if ($st === 'pending') return 'pending';

    if ($st === 'failed') {
        // canceled + ledger reversed
        updateSweepStatus($pdo, $sweepId, 'canceled', "TX failed: {$txHash}", false);
        updateLedgerStatus($pdo, $sweepId, LEDGER_REF_TYPE_SWEEP, 'reversed');
        return 'finalized';
    }

    // success: update state pending_wei and last_chain_balance_wei, ledger posted, sweep completed
    $pdo->beginTransaction();
    try {
        $lock = $pdo->prepare("
            SELECT pending_wei, last_chain_balance_wei
            FROM user_deposit_address_state
            WHERE uda_id=:uda AND asset_id=:aid AND network_id=:nid
            FOR UPDATE
        ");
        $lock->execute(['uda'=>$udaId,'aid'=>$assetId,'nid'=>$networkId]);
        $state = $lock->fetch(PDO::FETCH_ASSOC);

        if ($state) {
            $pendingWei = (string)($state['pending_wei'] ?? '0');
            $lastWei    = (string)($state['last_chain_balance_wei'] ?? '0');

            $newPending = (bccomp($pendingWei, $amtWei, 0) >= 0) ? bcsub($pendingWei, $amtWei, 0) : '0';
            $newLast    = (bccomp($lastWei, $amtWei, 0) >= 0) ? bcsub($lastWei, $amtWei, 0) : '0';

            $pdo->prepare("
                UPDATE user_deposit_address_state
                SET pending_wei=:p,
                    last_chain_balance_wei=:lb,
                    last_credit_at=NOW(),
                    last_sweep_at=NOW(),
                    last_seen_at=NOW(),
                    updated_at=NOW()
                WHERE uda_id=:uda AND asset_id=:aid AND network_id=:nid
            ")->execute(['p'=>$newPending,'lb'=>$newLast,'uda'=>$udaId,'aid'=>$assetId,'nid'=>$networkId]);
        }

        updateLedgerStatus($pdo, $sweepId, LEDGER_REF_TYPE_SWEEP, 'posted');
        updateSweepStatus($pdo, $sweepId, 'completed', "TX confirmed: {$txHash}", true);

        $pdo->commit();
    } catch (Throwable $e) {
        $pdo->rollBack();
        updateSweepStatus($pdo, $sweepId, 'canceled', "Finalize error: ".$e->getMessage(), false);
        yc_scan_log("FINALIZE_SWEEP_ERROR: ".$e->getMessage()." tx=".$txHash);
        return 'finalized';
    }

    // بعد تأكيد السويب: حاول إرجاع فائض الغاز إلى محفظة الغاز
    try {
        $dIndex = (int)($t['derivation_index'] ?? 0);
        if ($depAddr !== '' && $dIndex > 0 && $depAddr !== strtolower($gasAddress)) {
            $w = eth_wallet_from_index($mnemonic, $dIndex);
            $fromPk = $w['private_key'];

            $refund = reclaimExcessNativeToGasWallet(
                $rpcEndpoint, $chainId,
                $depAddr, $fromPk,
                $gasAddress,
                $minRefundWeiDec,
                DEFAULT_GAS_LIMIT_NATIVE
            );

            if ($refund) {
                // سجل في gas_wallet_operations
                $gasBalWei = getNativeBalanceWeiDec($rpcEndpoint, $gasAddress);
                $gasBalDec = weiToDecimal($gasBalWei, 18, 18);

                $opId = insertGasOp($pdo, [
                    'network_id' => (int)$t['network_id'],
                    'network_code' => (string)$t['network_code'],
                    'chain_id' => $chainId,
                    'gas_wallet_address' => strtolower($gasAddress),
                    'op_type' => 'gas_refund',
                    'direction' => 'in',
                    'from_address' => $depAddr,
                    'to_address' => strtolower($gasAddress),
                    'amount_wei' => (string)$refund['send_wei'],
                    'amount_native' => weiToDecimal((string)$refund['send_wei'], 18, 18),
                    'tx_hash' => (string)$refund['tx_hash'],
                    'request_status' => 'pending',
                    'gas_wallet_balance_after_wei' => $gasBalWei,
                    'gas_wallet_balance_after_native' => $gasBalDec,
                    'note' => 'Gas refund broadcasted after finalize',
                ]);

                // تأكيد خفيف
                $rt = 4; $final = 'pending';
                for ($k=0; $k<$rt; $k++) {
                    $rs = getTxReceiptStatus($rpcEndpoint, (string)$refund['tx_hash']);
                    if ($rs === 'pending') { sleep(2); continue; }
                    if ($rs === 'failed')  { $final = 'canceled'; break; }
                    if ($rs === 'success') { $final = 'completed'; break; }
                }
                if ($final !== 'pending') {
                    updateGasOpStatus($pdo, $opId, $final, "Gas refund {$final}: ".$refund['tx_hash'], $final==='completed');
                }
            }
        }
    } catch (Throwable $e) {
        yc_scan_log("GAS_REFUND_FINALIZE_ERROR: ".$e->getMessage()." tx=".$txHash);
    }

    return 'finalized';
}

/* ================== Run ================== */
foreach ($targets as $t) {
    $udaId   = (int)$t['uda_id'];
    $userId  = (int)$t['user_id'];
    $assetId = (int)$t['asset_id'];
    $netId   = (int)$t['network_id'];
    $netCode = (string)$t['network_code'];

    $address = strtolower(trim((string)$t['address']));
    $dIndex  = (int)($t['derivation_index'] ?? 0);

    if ($address === '' || !str_starts_with($address, '0x') || strlen($address) !== 42) continue;
    if ($dIndex <= 0) {
        yc_scan_log("Missing derivation_index uda={$udaId}");
        continue;
    }

    $rpcEndpoint = trim((string)($t['rpc_endpoint'] ?? ''));
    if ($rpcEndpoint === '') $rpcEndpoint = DEFAULT_RPC_ENDPOINT;

    $chainId = (int)($t['chain_id'] ?? 0);
    if ($chainId <= 0) $chainId = DEFAULT_CHAIN_ID;

    $isNative = ((int)$t['is_native'] === 1);

    $contract = strtolower(trim((string)($t['contract_address'] ?? '')));
    $decimalsOnchain = (int)($t['decimals_onchain'] ?? 0);
    if ($decimalsOnchain <= 0) $decimalsOnchain = 18;

    $minDepositStr = (string)($t['min_deposit'] ?? '0');
    $minWei = decimalToWei($minDepositStr, $decimalsOnchain);

    $gasLimit = (int)($t['gas_limit'] ?? 0);
    if ($gasLimit <= 0) $gasLimit = $isNative ? DEFAULT_GAS_LIMIT_NATIVE : DEFAULT_GAS_LIMIT_TOKEN;

    $alwaysTopup = true;
    if (isset($t['always_topup_gas'])) $alwaysTopup = ((int)$t['always_topup_gas'] === 1);

    $topupStr = trim((string)($t['gas_topup_native'] ?? ''));
    if ($topupStr === '' || !preg_match('/^\d+(\.\d+)?$/', $topupStr)) $topupStr = $envTopupStr;
    if ($topupStr === '' || !preg_match('/^\d+(\.\d+)?$/', $topupStr)) $topupStr = DEFAULT_GAS_TOPUP_NATIVE;
    $topupWei = decimalToWei($topupStr, 18);

    $minRefundWei = decimalToWei($envRefundMinStr, 18);

    // 1) finalize pending sweep (إن وجد)
    $finalizeRes = finalizePendingSweepIfAny($pdo, $t, $rpcEndpoint, $mnemonic, $chainId, $gasAddress, $minRefundWei);
    $blockSweep = ($finalizeRes === 'pending'); // نمنع sweep جديد إذا السابق pending

    // 2) read current onchain balance
    $currentWei = null;
    if ($isNative) {
        $currentWei = getNativeBalanceWeiDec($rpcEndpoint, $address);
    } else {
        if ($contract === '') continue;
        $b = getTokenBalanceWeiDec($rpcEndpoint, $contract, $address);
        if ($b === null) continue;
        $currentWei = $b;
    }
    $currentAmt = weiToDecimal($currentWei, $decimalsOnchain, 18);

    // 3) state + delta + pending
    $pdo->beginTransaction();
    try {
        ensureStateRow($pdo, [
            'uda_id'=>$udaId,'user_id'=>$userId,'asset_id'=>$assetId,'network_id'=>$netId,'network_code'=>$netCode,'address'=>$address
        ]);

        $lock = $pdo->prepare("
            SELECT id, last_chain_balance_wei, pending_wei
            FROM user_deposit_address_state
            WHERE uda_id=:uda AND asset_id=:aid AND network_id=:nid
            FOR UPDATE
        ");
        $lock->execute(['uda'=>$udaId,'aid'=>$assetId,'nid'=>$netId]);
        $state = $lock->fetch(PDO::FETCH_ASSOC);
        if (!$state) { $pdo->rollBack(); continue; }

        $stateId = (int)$state['id'];
        $lastWei = (string)($state['last_chain_balance_wei'] ?? '0');
        $pendingWei = (string)($state['pending_wei'] ?? '0');

        $deltaWei = '0';
        if (bccomp($currentWei, $lastWei, 0) > 0) $deltaWei = bcsub($currentWei, $lastWei, 0);

        $didDetect = (bccomp($deltaWei, '0', 0) > 0);

        $newPendingWei = $pendingWei;
        if ($didDetect) $newPendingWei = bcadd($pendingWei, $deltaWei, 0);

        // update state
        $pdo->prepare("
            UPDATE user_deposit_address_state
            SET last_chain_balance_wei=:bal,
                pending_wei=:p,
                last_seen_at=NOW(),
                updated_at=NOW()
            WHERE uda_id=:uda AND asset_id=:aid AND network_id=:nid
        ")->execute(['bal'=>$currentWei,'p'=>$newPendingWei,'uda'=>$udaId,'aid'=>$assetId,'nid'=>$netId]);

        // سجل detected في جدول crypto_deposit_detections
        if ($didDetect) {
            insertDetection($pdo, [
                'state_id'=>$stateId,
                'uda_id'=>$udaId,'user_id'=>$userId,'asset_id'=>$assetId,'network_id'=>$netId,'network_code'=>$netCode,
                'deposit_address'=>$address,
                'event_type'=>'detected',
                'delta_wei'=>$deltaWei,
                'delta_amount'=>weiToDecimal($deltaWei, $decimalsOnchain, 18),
                'pending_after_wei'=>$newPendingWei,
                'pending_after_amount'=>weiToDecimal($newPendingWei, $decimalsOnchain, 18),
                'current_balance_wei'=>$currentWei,
                'current_balance_amount'=>$currentAmt,
                'min_deposit'=>$minDepositStr,
                'note'=>'Deposit increase detected',
            ]);
        }

        // below_min => سجل نوع below_min ثم اخرج
        if (bccomp($newPendingWei, $minWei, 0) < 0) {
            if ($didDetect) {
                insertDetection($pdo, [
                    'state_id'=>$stateId,
                    'uda_id'=>$udaId,'user_id'=>$userId,'asset_id'=>$assetId,'network_id'=>$netId,'network_code'=>$netCode,
                    'deposit_address'=>$address,
                    'event_type'=>'below_min',
                    'delta_wei'=>null,
                    'delta_amount'=>null,
                    'pending_after_wei'=>$newPendingWei,
                    'pending_after_amount'=>weiToDecimal($newPendingWei, $decimalsOnchain, 18),
                    'current_balance_wei'=>$currentWei,
                    'current_balance_amount'=>$currentAmt,
                    'min_deposit'=>$minDepositStr,
                    'note'=>'Below min_deposit: recorded only, no sweep',
                ]);
            }
            $pdo->commit();
            continue;
        }

        $pdo->commit();

        // إذا يوجد sweep pending سابق: لا تعمل sweep جديد الآن
        if ($blockSweep) continue;

        // 4) Gas topup (Always) قبل sweep
        if ($alwaysTopup && bccomp($topupWei, '0', 0) > 0 && strtolower($address) !== strtolower($gasAddress)) {
            $gasBal = getNativeBalanceWeiDec($rpcEndpoint, $gasAddress);
            if (bccomp($gasBal, $topupWei, 0) <= 0) {
                // سجل عملية غاز canceled (فشل بسبب رصيد غير كافي)
                insertGasOp($pdo, [
                    'network_id'=>$netId,
                    'network_code'=>$netCode,
                    'chain_id'=>$chainId,
                    'gas_wallet_address'=>strtolower($gasAddress),
                    'op_type'=>'gas_topup',
                    'direction'=>'out',
                    'from_address'=>strtolower($gasAddress),
                    'to_address'=>$address,
                    'amount_wei'=>$topupWei,
                    'amount_native'=>weiToDecimal($topupWei, 18, 18),
                    'tx_hash'=>null,
                    'request_status'=>'canceled',
                    'gas_wallet_balance_after_wei'=>$gasBal,
                    'gas_wallet_balance_after_native'=>weiToDecimal($gasBal, 18, 18),
                    'note'=>'Gas wallet balance insufficient for topup',
                ]);
                continue;
            }

            $txGas = sendNativeTransferTx(
                $rpcEndpoint, $chainId,
                $gasAddress, $gasPrivKey,
                $address, $topupWei,
                DEFAULT_GAS_LIMIT_NATIVE
            );

            $gasBalAfter = getNativeBalanceWeiDec($rpcEndpoint, $gasAddress);
            $gasBalAfterDec = weiToDecimal($gasBalAfter, 18, 18);

            $gasOpId = insertGasOp($pdo, [
                'network_id'=>$netId,
                'network_code'=>$netCode,
                'chain_id'=>$chainId,
                'gas_wallet_address'=>strtolower($gasAddress),
                'op_type'=>'gas_topup',
                'direction'=>'out',
                'from_address'=>strtolower($gasAddress),
                'to_address'=>$address,
                'amount_wei'=>$topupWei,
                'amount_native'=>weiToDecimal($topupWei, 18, 18),
                'tx_hash'=>$txGas,
                'request_status'=>$txGas ? 'pending' : 'canceled',
                'gas_wallet_balance_after_wei'=>$gasBalAfter,
                'gas_wallet_balance_after_native'=>$gasBalAfterDec,
                'note'=>$txGas ? "Gas topup broadcasted ({$topupStr} Native)" : "Gas topup failed to broadcast",
            ]);

            if (!$txGas) continue;

            // تأكيد خفيف للتوب أب (اختياري)
            $rt = 4; $final = 'pending';
            for ($k=0; $k<$rt; $k++) {
                $rs = getTxReceiptStatus($rpcEndpoint, $txGas);
                if ($rs === 'pending') { sleep(2); continue; }
                if ($rs === 'failed')  { $final = 'canceled'; break; }
                if ($rs === 'success') { $final = 'completed'; break; }
            }
            if ($final !== 'pending') {
                updateGasOpStatus($pdo, $gasOpId, $final, "Gas topup {$final}: ".$txGas, $final==='completed');
                if ($final === 'canceled') continue;
            }

            sleep(4);
        }

        // 5) تحديد sweepWei حسب pending و balanceNow
        $balanceNowWei = null;
        if ($isNative) {
            $balanceNowWei = getNativeBalanceWeiDec($rpcEndpoint, $address);
        } else {
            $balanceNowWei = getTokenBalanceWeiDec($rpcEndpoint, $contract, $address);
            if ($balanceNowWei === null) continue;
        }

        $sweepWei = (bccomp($newPendingWei, $balanceNowWei, 0) <= 0) ? $newPendingWei : $balanceNowWei;

        // إذا Native: اترك gas للرسوم
        if ($isNative) {
            $gasPriceWei = getGasPriceWeiDec($rpcEndpoint);
            if ($gasPriceWei === null) {
                yc_scan_log("Failed eth_gasPrice for native sweep uda={$udaId}");
                continue;
            }
            $requiredWei = bcmul($gasPriceWei, (string)$gasLimit, 0);
            $bufferWei   = bcdiv($requiredWei, '5', 0);
            $keepWei     = bcadd($requiredWei, $bufferWei, 0);

            if (bccomp($balanceNowWei, $keepWei, 0) <= 0) {
                yc_scan_log("Native balance not enough after keeping gas uda={$udaId}");
                continue;
            }
            $maxSweep = bcsub($balanceNowWei, $keepWei, 0);
            if (bccomp($sweepWei, $maxSweep, 0) > 0) $sweepWei = $maxSweep;
        }

        if (bccomp($sweepWei, $minWei, 0) < 0 || bccomp($sweepWei, '0', 0) <= 0) {
            yc_scan_log("Sweep amount < min_deposit or zero uda={$udaId}");
            continue;
        }

        // 6) تنفيذ Sweep
        $w = eth_wallet_from_index($mnemonic, $dIndex);
        $fromPk = $w['private_key'];

        $txSweep = null;
        if ($isNative) {
            $txSweep = sendNativeTransferTx(
                $rpcEndpoint, $chainId,
                $address, $fromPk,
                $collectAddress, $sweepWei,
                $gasLimit
            );
        } else {
            $txSweep = sendTokenTransferTx(
                $rpcEndpoint, $chainId,
                $address, $fromPk,
                $contract, $collectAddress,
                $sweepWei,
                $gasLimit
            );
        }

        $amountDec = weiToDecimal($sweepWei, $decimalsOnchain, 18);

        // سجّل sweep في crypto_sweep_operations (حتى لو فشل broadcast نسجله canceled)
        $sweepId = insertSweep($pdo, [
            'state_id'=>null,
            'uda_id'=>$udaId,'user_id'=>$userId,'asset_id'=>$assetId,'network_id'=>$netId,'network_code'=>$netCode,
            'deposit_address'=>$address,
            'collect_address'=>$collectAddress,
            'amount_wei'=>$sweepWei,
            'amount_decimal'=>$amountDec,
            'tx_hash'=>$txSweep,
            'request_status'=>$txSweep ? 'pending' : 'canceled',
            'ledger_ref_type'=>LEDGER_REF_TYPE_SWEEP,
            'ledger_ref_id'=>null,
            'note'=>$txSweep ? 'Sweep broadcasted' : 'Sweep failed to broadcast',
        ]);

        if (!$txSweep) continue;

        // 7) ledger pending مرتبط بالسويب
        $note = "Crypto sweep {$t['asset_code']} ({$netCode}) | from={$address} | to={$collectAddress} | tx={$txSweep}";
        insertLedger($pdo, $note, 'pending', $sweepId, LEDGER_REF_TYPE_SWEEP, 'available', $amountDec, $assetId, $userId);

        // 8) محاولة تأكيد سريع للسويب
        $tries = 6;
        for ($i=0; $i<$tries; $i++) {
            $st = getTxReceiptStatus($rpcEndpoint, $txSweep);
            if ($st === 'pending') { sleep(3); continue; }

            if ($st === 'failed') {
                updateSweepStatus($pdo, $sweepId, 'canceled', "TX failed: {$txSweep}", false);
                updateLedgerStatus($pdo, $sweepId, LEDGER_REF_TYPE_SWEEP, 'reversed');
                break;
            }

            // success => خصم pending + FIX last_chain_balance_wei + ledger posted + sweep completed
            $pdo->beginTransaction();
            try {
                $lock2 = $pdo->prepare("
                    SELECT pending_wei, last_chain_balance_wei
                    FROM user_deposit_address_state
                    WHERE uda_id=:uda AND asset_id=:aid AND network_id=:nid
                    FOR UPDATE
                ");
                $lock2->execute(['uda'=>$udaId,'aid'=>$assetId,'nid'=>$netId]);
                $stRow = $lock2->fetch(PDO::FETCH_ASSOC);

                if ($stRow) {
                    $pend = (string)($stRow['pending_wei'] ?? '0');
                    $last = (string)($stRow['last_chain_balance_wei'] ?? '0');

                    $newPend = (bccomp($pend, $sweepWei, 0) >= 0) ? bcsub($pend, $sweepWei, 0) : '0';
                    $newLast = (bccomp($last, $sweepWei, 0) >= 0) ? bcsub($last, $sweepWei, 0) : '0';

                    $pdo->prepare("
                        UPDATE user_deposit_address_state
                        SET pending_wei=:p,
                            last_chain_balance_wei=:lb,
                            last_credit_at=NOW(),
                            last_sweep_at=NOW(),
                            last_seen_at=NOW(),
                            updated_at=NOW()
                        WHERE uda_id=:uda AND asset_id=:aid AND network_id=:nid
                    ")->execute(['p'=>$newPend,'lb'=>$newLast,'uda'=>$udaId,'aid'=>$assetId,'nid'=>$netId]);
                }

                updateLedgerStatus($pdo, $sweepId, LEDGER_REF_TYPE_SWEEP, 'posted');
                updateSweepStatus($pdo, $sweepId, 'completed', "TX confirmed: {$txSweep}", true);

                $pdo->commit();
            } catch (Throwable $e) {
                $pdo->rollBack();
                updateSweepStatus($pdo, $sweepId, 'canceled', "Finalize-in-loop error: ".$e->getMessage(), false);
                yc_scan_log("CONFIRM_LOOP_ERROR: ".$e->getMessage()." tx=".$txSweep);
                break;
            }

            // 9) بعد تأكيد السويب: gas_refund إلى محفظة الغاز + تسجيله في gas_wallet_operations
            try {
                if (strtolower($address) !== strtolower($gasAddress)) {
                    $refund = reclaimExcessNativeToGasWallet(
                        $rpcEndpoint,
                        $chainId,
                        $address,
                        $fromPk,
                        $gasAddress,
                        $minRefundWei,
                        DEFAULT_GAS_LIMIT_NATIVE
                    );

                    if ($refund) {
                        $gasBalWei = getNativeBalanceWeiDec($rpcEndpoint, $gasAddress);
                        $gasBalDec = weiToDecimal($gasBalWei, 18, 18);

                        $refundOpId = insertGasOp($pdo, [
                            'network_id'=>$netId,
                            'network_code'=>$netCode,
                            'chain_id'=>$chainId,
                            'gas_wallet_address'=>strtolower($gasAddress),
                            'op_type'=>'gas_refund',
                            'direction'=>'in',
                            'from_address'=>$address,
                            'to_address'=>strtolower($gasAddress),
                            'amount_wei'=>(string)$refund['send_wei'],
                            'amount_native'=>weiToDecimal((string)$refund['send_wei'], 18, 18),
                            'tx_hash'=>(string)$refund['tx_hash'],
                            'request_status'=>'pending',
                            'gas_wallet_balance_after_wei'=>$gasBalWei,
                            'gas_wallet_balance_after_native'=>$gasBalDec,
                            'note'=>'Gas refund broadcasted after sweep confirmation',
                        ]);

                        // تأكيد خفيف للـ refund
                        $rt = 4; $final = 'pending';
                        for ($k=0; $k<$rt; $k++) {
                            $rs = getTxReceiptStatus($rpcEndpoint, (string)$refund['tx_hash']);
                            if ($rs === 'pending') { sleep(2); continue; }
                            if ($rs === 'failed')  { $final = 'canceled'; break; }
                            if ($rs === 'success') { $final = 'completed'; break; }
                        }
                        if ($final !== 'pending') {
                            updateGasOpStatus($pdo, $refundOpId, $final, "Gas refund {$final}: ".$refund['tx_hash'], $final==='completed');
                        }
                    }
                }
            } catch (Throwable $e) {
                yc_scan_log("GAS_REFUND_ERROR: ".$e->getMessage()." tx=".$txSweep);
            }

            break;
        }

    } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        yc_scan_log("RUN_ERROR: ".$e->getMessage()." uda={$udaId} asset={$assetId} net={$netId}");
        continue;
    }
}

out("Done.\n");
