<?php
declare(strict_types=1);

namespace LifeSwap;

use Web3\Web3;
use Web3\Contract;
use Web3\Providers\HttpProvider;

class Web3Client
{
    public Web3 $web3;
    private string $from;
    private string $pk;

    // local nonce session (hex quantity like 0x1a)
    private ?string $localNonce = null;

    public function __construct()
    {
        $rpc = Env::get('RPC_URL');
        $this->pk = Env::get('PRIVATE_KEY');

        // Accept FROM or FROM_ADDRESS; normalize + validate
        $from = Env::get('FROM', Env::get('FROM_ADDRESS', ''));
        $from = strtolower(trim($from));
        if (!preg_match('/^0x[0-9a-f]{40}$/', $from)) {
            throw new \RuntimeException("Invalid FROM address. Set FROM=0x... (40 hex chars) in .env");
        }
        $this->from = $from;

        $this->web3 = new Web3(new HttpProvider($rpc));
    }

    public function getFrom(): string { return $this->from; }

    public function contract(array|string $abi, string $address): Contract
    {
        return new Contract($this->web3->provider, $abi, $address);
    }

    public function await(callable $fn)
    {
        $result = null;
        $fn(function ($err, $res) use (&$result) {
            if ($err !== null) {
                $msg = is_string($err) ? $err : ($err->getMessage() ?? 'RPC error');
                throw new \RuntimeException($msg);
            }
            $result = $res;
        });
        return $result;
    }

    private function hex(callable $await)
    {
        $v = $this->await($await);
        return \LifeSwap\Hex::quantityToHex($v);
    }

    public function ethCall(string $to, string $data): string
    {
        $to = strtolower(trim($to));
        if (!str_starts_with($data, '0x') && !str_starts_with($data, '0X')) {
            $data = '0x' . ltrim($data, '0x');
        }
        $out = '0x';
        $this->web3->eth->call(['to' => $to, 'data' => $data], function ($err, $res) use (&$out) {
            if ($err) {
                throw new \RuntimeException($err->getMessage());
            }
            $out = is_string($res) ? strtolower($res) : '0x';
        });
        return $out;
    }

    /* ---------- Nonce session helpers ---------- */

    public function beginNonce(): void
    {
        if ($this->localNonce === null) {
            $eth = $this->web3->eth;
            $n = $this->hex(fn($cb) => $eth->getTransactionCount($this->from, 'pending', $cb));
            $this->localNonce = $n; // hex quantity
        }
    }

    private function takeNonce(): string
    {
        if ($this->localNonce === null) $this->beginNonce();
        $nHex = $this->localNonce;
        $dec = hexdec(\LifeSwap\Hex::strip0x($nHex));
        $this->localNonce = '0x' . dechex($dec + 1);
        return $nHex;
    }

    /**
     * Send a raw signed transaction (legacy/EIP-155 style for BSC).
     * Bumps gas on "replacement transaction underpriced".
     */
    public function sendRaw(
        string $to,
        string $data,
        string $valueWei = '0x0',
        ?string $gasLimit = null,
        bool $withDebug = false
    ) {
        $eth = $this->web3->eth;

        // local nonce sequencing
        $nonce = $this->takeNonce();

        // gas
        $nodeGas = $this->hex(fn($cb) => $eth->gasPrice($cb));
        $floorGwei = (int)Env::get('GAS_PRICE_FLOOR_GWEI', '3');
        $minGas = '0x' . dechex($floorGwei * 1000_000_000);

        $gasPrice = (hexdec(\LifeSwap\Hex::strip0x($nodeGas)) < hexdec(\LifeSwap\Hex::strip0x($minGas)))
            ? $minGas
            : $nodeGas;

        $gas   = $gasLimit ?? '0x' . dechex((int)Env::get('GAS_LIMIT', '550000'));
        $chainId = (int)Env::get('CHAIN_ID', '97');

        $build = function ($nonceHex, $gasPriceHex) use ($to, $data, $valueWei, $gas, $chainId) {
            return \LifeSwap\TxSigner::signLegacy($this->pk, [
                'nonce'    => $nonceHex,
                'gasPrice' => $gasPriceHex,
                'gas'      => $gas,
                'to'       => $to,
                'value'    => $valueWei,
                'data'     => $data
            ], $chainId);
        };

        $bumpBps  = max(100, (int)Env::get('GAS_BUMP_BPS', '1500')); // default +15%
        $maxRetry = max(1, (int)Env::get('GAS_MAX_RETRIES', '3'));

        $attempts = 0;
        $lastGas  = $gasPrice;
        while (true) {
            $raw = $build($nonce, $lastGas);
            try {
                $txHash = $this->await(fn($cb) => $eth->sendRawTransaction($raw, $cb));
                break;
            } catch (\RuntimeException $e) {
                $msg = strtolower($e->getMessage());
                if (strpos($msg, 'replacement transaction underpriced') !== false && $attempts < $maxRetry - 1) {
                    $g = hexdec(\LifeSwap\Hex::strip0x($lastGas));
                    $g = (int)ceil($g * (1 + $bumpBps / 10_000));
                    $lastGas = '0x' . dechex($g);
                    $attempts++;
                    continue;
                }
                if (strpos($msg, 'nonce too low') !== false && $attempts < $maxRetry - 1) {
                    $this->localNonce = null; // refresh
                    $nonce = $this->takeNonce();
                    $attempts++;
                    continue;
                }
                throw $e;
            }
        }

        if ($withDebug) {
            return [
                'txHash' => $txHash,
                'txPreview' => [
                    'from'     => $this->from,
                    'to'       => $to,
                    'nonce'    => $nonce,
                    'gasPrice' => $lastGas,
                    'gas'      => $gas,
                    'value'    => $valueWei,
                    'dataLen'  => strlen(\LifeSwap\Hex::strip0x($data)),
                    'chainId'  => $chainId
                ]
            ];
        }
        return $txHash;
    }
}