<?php
namespace Bot\Services;

use Bot\Config;
use Bot\Database;
use GuzzleHttp\Client;
use Web3p\EthereumTx\Transaction;
use PDO;

class WithdrawalProcessor
{
    private PDO $pdo;
    private BalanceService $balances;
    private WithdrawalService $withdrawals;
    private EncryptionService $encryption;
    private Client $http;
    private array $tokens;
    private string $rpcUrl;
    private int $chainId;
    private int $gasLimit;
    private int $gasPriceGwei;

    public function __construct()
    {
        $config = Config::get();
        $this->pdo = Database::connection();
        $this->balances = new BalanceService();
        $this->withdrawals = new WithdrawalService();
        $this->encryption = new EncryptionService();
        $this->http = new Client(['timeout' => 15]);
        $this->tokens = $config->getValue('watched_tokens', []);
        $this->rpcUrl = (string) $config->getValue('alchemy_api_url', '');
        $this->chainId = (int) ($config->getValue('evm_chain_id', 1) ?? 1);
        $this->gasLimit = (int) ($config->getValue('evm_gas_limit', 120000) ?? 120000);
        $this->gasPriceGwei = (int) ($config->getValue('evm_gas_price_gwei', 5) ?? 5);
    }

    public function processQueue(int $limit = 5): int
    {
        if ($this->rpcUrl === '') {
            return 0;
        }
        $pending = $this->withdrawals->getPending($limit);
        if (!$pending) {
            return 0;
        }
        $processed = 0;
        foreach ($pending as $row) {
            $id = (int) $row['id'];
            if (!$this->withdrawals->markProcessing($id)) {
                continue;
            }
            try {
                if (strtoupper($row['network']) !== 'ERC20') {
                    throw new \RuntimeException('Network not supported yet.');
                }
                $txHash = $this->sendErc20Transfer($row);
                $this->withdrawals->markBroadcasted($id, $txHash);
                $this->balances->consumeLocked((int) $row['user_id'], $row['currency'], $row['amount']);
                $processed++;
            } catch (\Throwable $e) {
                $this->withdrawals->markFailed($id, $e->getMessage());
                $this->balances->unlock((int) $row['user_id'], $row['currency'], $row['amount']);
            }
        }
        return $processed;
    }

    private function sendErc20Transfer(array $withdrawal): string
    {
        $token = $this->getTokenConfig($withdrawal['currency'], 'ERC20');
        $contract = $token['address'];
        if ($contract === '') {
            throw new \RuntimeException('Token contract missing.');
        }
        $decimals = (int) $token['decimals'];
        $privateKey = $this->encryption->decrypt($withdrawal['private_key_encrypted']);
        $from = $withdrawal['wallet_address'];
        $to = $withdrawal['to_address'];
        $nonce = $this->fetchNonce($from);
        $gasPriceWei = $this->gasPriceGwei * 1_000_000_000;
        $value = $this->scaleUp($withdrawal['amount'], $decimals);
        $data = $this->buildTransferData($to, $value);
        $txParams = [
            'nonce'    => '0x' . dechex($nonce),
            'gasPrice' => '0x' . dechex($gasPriceWei),
            'gas'      => '0x' . dechex($this->gasLimit),
            'to'       => $contract,
            'value'    => '0x0',
            'data'     => $data,
            'chainId'  => $this->chainId,
        ];
        $transaction = new Transaction($txParams);
        $signed = $transaction->sign($privateKey);
        $payload = '0x' . $signed;
        $result = $this->rpc('eth_sendRawTransaction', [$payload]);
        if (!isset($result['result'])) {
            throw new \RuntimeException('Broadcast failed: ' . json_encode($result));
        }
        return $result['result'];
    }

    private function fetchNonce(string $address): int
    {
        $result = $this->rpc('eth_getTransactionCount', [$address, 'pending']);
        if (!isset($result['result'])) {
            throw new \RuntimeException('Unable to fetch nonce.');
        }
        return hexdec($result['result']);
    }

    private function rpc(string $method, array $params): array
    {
        $payload = [
            'jsonrpc' => '2.0',
            'id'      => random_int(1, 999999),
            'method'  => $method,
            'params'  => $params,
        ];
        $response = $this->http->post($this->rpcUrl, ['json' => $payload]);
        return json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
    }

    private function buildTransferData(string $to, string $amountHex): string
    {
        $method = 'a9059cbb';
        $address = str_pad(strtolower(ltrim($to, '0x')), 64, '0', STR_PAD_LEFT);
        $cleanValue = preg_replace('/^0x/i', '', $amountHex);
        $value = str_pad($cleanValue, 64, '0', STR_PAD_LEFT);
        return '0x' . $method . $address . $value;
    }

    private function scaleUp(string $amount, int $decimals): string
    {
        [$intPart, $fracPart] = explode('.', $amount . '.', 2);
        $fracPart = rtrim($fracPart, '0');
        if (strlen($fracPart) > $decimals) {
            $fracPart = substr($fracPart, 0, $decimals);
        }
        $fracPart = str_pad($fracPart, $decimals, '0');
        $number = ltrim($intPart . $fracPart, '0');
        $number = $number === '' ? '0' : $number;
        $hex = $this->decimalToHex($number);
        return '0x' . $hex;
    }

    private function getTokenConfig(string $currency, string $network): array
    {
        $symbol = strtoupper($currency);
        $config = $this->tokens[$symbol] ?? null;
        if (!$config || empty($config[$network])) {
            return [
                'address'  => '',
                'decimals' => 6,
            ];
        }
        return [
            'address'  => $config[$network],
            'decimals' => (int) ($config['decimals'] ?? 6),
        ];
    }

    private function decimalToHex(string $decimal): string
    {
        $decimal = ltrim($decimal, '0');
        if ($decimal === '' || $decimal === '0') {
            return '0';
        }
        $hexDigits = '0123456789abcdef';
        $hex = '';
        while ($decimal !== '0') {
            [$decimal, $remainder] = $this->divByInt($decimal, 16);
            $hex = $hexDigits[$remainder] . $hex;
        }
        return $hex;
    }

    /**
     * @return array{0:string,1:int}
     */
    private function divByInt(string $value, int $divisor): array
    {
        $result = '';
        $carry = 0;
        $length = strlen($value);
        for ($i = 0; $i < $length; $i++) {
            $current = $carry * 10 + (int) $value[$i];
            $quotient = intdiv($current, $divisor);
            $carry = $current % $divisor;
            if ($result !== '' || $quotient !== 0) {
                $result .= (string) $quotient;
            }
        }
        if ($result === '') {
            $result = '0';
        }
        return [$result, $carry];
    }
}
