<?php
declare(strict_types=1);

namespace LifeSwap;

use Web3\Contract;
use kornrunner\Keccak;

class SwapService
{
    private Web3Client $w3;
    private Contract $weth;    // WBNB (WETH9 interface)
    private Contract $router;  // SwapRouter v3 (ABI used only for deposit/approve via WBNB ABI when needed)

    public function __construct()
    {
        $this->w3     = new Web3Client();
        $this->weth   = $this->w3->contract(Abi::WETH9, Env::get('WBNB'));
        $this->router = $this->w3->contract(Abi::SWAPROUTER_V3, Env::get('SWAP_ROUTER_V3'));
    }

    /**
     * Main flow used by both endpoints: wrap BNB -> approve (if needed) -> exactInputSingle(WBNB->OUT_TOKEN).
     * Returns tx hashes and rich debug info.
     */
    public function swapBnbToTokenAndSend(string $to, string $amountBnb): array
    {
        $this->w3->beginNonce();

        $routerAddr = strtolower(Env::get('SWAP_ROUTER_V3'));
        $wbnb       = strtolower(Env::get('WBNB'));
        $out        = strtolower(Env::get('OUT_TOKEN'));
        $fee        = (int) Env::get('FEE_TIER', '500');

        $amountWei  = $this->toWei($amountBnb);
        $deadline   = time() + (int) Env::get('DEADLINE_SECS', '300');
        $maxUint    = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';

        // 1) BNB -> WBNB (deposit payable)
        $depositData = $this->weth->getData('deposit');
        $tx1 = $this->w3->sendRaw($wbnb, $depositData, $amountWei, null, true);

        // 2) Allowance: approve router if needed (or forced)
        $owner        = $this->w3->getFrom();
        $allowHex     = $this->callAllowance($wbnb, $owner, $routerAddr);
        $needsApprove = $this->lt($allowHex, $amountWei);
        $forceApprove = filter_var(Env::get('FORCE_APPROVE', 'false'), FILTER_VALIDATE_BOOLEAN);

        $tx2 = null;
        if ($needsApprove || $forceApprove) {
            $approveData = $this->weth->getData('approve', $routerAddr, $maxUint);
            $tx2 = $this->w3->sendRaw($wbnb, $approveData, '0x0', null, true);
        }

        // 3) exactInputSingle(WBNB -> OUT_TOKEN) — manual ABI to avoid tuple issues
        $minOutHex = '0x0'; // TODO: wire Quoter for slippage guard
        $swapData = $this->encodeExactInputSingle(
            $wbnb, $out, $fee, strtolower($to), (int)$deadline, $amountWei, $minOutHex, '0x0'
        );
        $tx3 = $this->w3->sendRaw($routerAddr, $swapData, '0x0', null, true);

        $wbnbBalHex = $this->callBalanceOf($wbnb, $owner);

        return [
            'ok'  => true,
            'txs' => [
                'deposit' => $tx1['txHash'],
                'approve' => $tx2 ? $tx2['txHash'] : ($needsApprove ? '(approve failed?)' : '(skipped: sufficient allowance or forced off)'),
                'swap'    => $tx3['txHash']
            ],
            'debug' => [
                'amountBnb'      => $amountBnb,
                'amountWeiHex'   => $amountWei,
                'router'         => $routerAddr,
                'feeTier'        => $fee,
                'allowanceHex'   => $allowHex,
                'wbnbBalanceHex' => $wbnbBalHex,
                'dataLens'       => [
                    'deposit' => $tx1['txPreview']['dataLen'] ?? null,
                    'approve' => $tx2 ? ($tx2['txPreview']['dataLen'] ?? null) : null,
                    'swap'    => $tx3['txPreview']['dataLen'] ?? null
                ],
                'preview' => [
                    'deposit' => $tx1['txPreview'],
                    'approve' => $tx2 ? $tx2['txPreview'] : null,
                    'swap'    => $tx3['txPreview']
                ]
            ]
        ];
    }

    /* ===================== Quoter (manual encoding) ===================== */

    /** Public for swap_zar.php - Quote exact OUTPUT (LIFE tokens) for BNB input */
    public function quoteAmountInForLife(string $lifeOutWeiHex, int $fee): string
    {
        $quoterAddr = strtolower(Env::get('QUOTER_V2'));
        $wbnb = strtolower(Env::get('WBNB'));
        $life = strtolower(Env::get('OUT_TOKEN'));

        // encode: quoteExactOutputSingle((address,address,uint256,uint24,uint160))
        $data = $this->encodeQuoteExactOutputSingle($wbnb, $life, $lifeOutWeiHex, $fee, '0x0');

        // call quoter
        $ret = $this->w3->ethCall($quoterAddr, $data);

        // first 32 bytes = amountIn
        $hex = \LifeSwap\Hex::strip0x($ret);
        $amountIn = substr($hex, 0, 64);
        return '0x' . ltrim($amountIn, '0');
    }

    /** NEW: Quote exact INPUT (BNB) for LIFE tokens output */
    public function quoteLifeForAmountIn(string $bnbInWeiHex, int $fee): string
    {
        $quoterAddr = strtolower(Env::get('QUOTER_V2'));
        $wbnb = strtolower(Env::get('WBNB'));
        $life = strtolower(Env::get('OUT_TOKEN'));

        // encode: quoteExactInputSingle(address,address,uint24,uint256,uint160)
        $data = $this->encodeQuoteExactInputSingle($wbnb, $life, $fee, $bnbInWeiHex, '0x0');

        // call quoter
        $ret = $this->w3->ethCall($quoterAddr, $data);

        // first 32 bytes = amountOut
        $hex = \LifeSwap\Hex::strip0x($ret);
        $amountOut = substr($hex, 0, 64);
        return '0x' . ltrim($amountOut, '0');
    }
	
	public function findV3Pool(string $tokenA, string $tokenB, int $fee): string {
		$factory = '0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865'; // PancakeV3Factory (per docs)
		// function selector for getPool(address,address,uint24)
		$sel = substr(\kornrunner\Keccak::hash('getPool(address,address,uint24)', 256), 0, 8);
		$wordAddr = fn($a) => \LifeSwap\Hex::padLeft(\LifeSwap\Hex::padLeft(\LifeSwap\Hex::strip0x($a), 20), 32);
		$wordFee  = \LifeSwap\Hex::padLeft(dechex($fee), 32);
		$data = '0x' . $sel . $wordAddr($tokenA) . $wordAddr($tokenB) . $wordFee;

		$ret = $this->w3->ethCall($factory, $data);
		$h = \LifeSwap\Hex::strip0x($ret);
		$pool = '0x' . substr(\LifeSwap\Hex::padLeft($h, 64), 24, 40);
		return strtolower($pool);
	}

    /* ===================== Feed Registry (Space ID) ===================== */

    /** Public for swap_zar.php — resolves FEED_REGISTRY (name or address), reads BNB/USD. */
	   public function readBnbUsdFromRegistry(): array
	{
		// Try new API-based price feeds first (much more reliable)
		try {
			return $this->readBnbUsdFromApi();
		} catch (Throwable $e) {
			error_log("API price feed failed: " . $e->getMessage() . ", falling back to Chainlink");
		}

		// Fallback to original Chainlink registry (often fails)
		$regVal = Env::get('FEED_REGISTRY');
		if (empty($regVal)) {
			throw new \RuntimeException('FEED_REGISTRY is not defined in .env');
		}
		$registry = $this->resolveSpaceIdToAddress($regVal); // resolves "fr.boracle.bnb" → 0x...

		// ---- function selectors (4 bytes) ----
		$selDecimalsByName = $this->selector('decimalsByName(string,string)');
		$selLatestByName   = $this->selector('latestRoundDataByName(string,string)');

		// ---- helper to encode two dynamic strings ("BNB","USD") ----
		$encodeTwoStrings = function (string $s1, string $s2): string {
			// head: offset1 (32), offset2 (64)
			$head  = str_pad(dechex(64), 64, '0', STR_PAD_LEFT);  // first arg offset = 0x40
			$head .= str_pad(dechex(64 + self::paddedLen($s1)), 64, '0', STR_PAD_LEFT); // second offset

			// tail: enc(s1) + enc(s2)
			$tail  = self::encodeStringWord($s1);
			$tail .= self::encodeStringWord($s2);

			return $head . $tail;
		};

		// Build calldata for decimalsByName("BNB","USD")
		$dataDec = '0x' . $selDecimalsByName . $encodeTwoStrings('BNB', 'USD');
		$decRes  = $this->w3->ethCall($registry, $dataDec);
		$dec     = hexdec(substr(\LifeSwap\Hex::strip0x($decRes), 0, 64));

		// Build calldata for latestRoundDataByName("BNB","USD")
		$dataLR  = '0x' . $selLatestByName . $encodeTwoStrings('BNB', 'USD');
		$lrRes   = $this->w3->ethCall($registry, $dataLR);
		$hex     = \LifeSwap\Hex::strip0x($lrRes);

		// latestRoundDataByName returns: roundId, answer (int256), startedAt, updatedAt, answeredInRound
		$answerHex = substr($hex, 64, 64);
		$answerDec = \LifeSwap\Hex::hexToDecString($answerHex, true);

		return ['price' => $answerDec, 'decimals' => $dec, 'raw' => $lrRes, 'registry' => $registry];
	}

	/** New reliable BNB/USD price method using CoinGecko and Binance APIs */
	public function readBnbUsdFromApi(): array
	{
		$sources = [
			'coingecko' => 'https://api.coingecko.com/api/v3/simple/price?ids=binancecoin&vs_currencies=usd',
			'binance' => 'https://api.binance.com/api/v3/ticker/price?symbol=BNBUSDT'
		];

		$lastError = null;

		foreach ($sources as $source => $url) {
			try {
				$context = stream_context_create([
					'http' => [
						'method' => 'GET',
						'header' => [
							'User-Agent: LifeSwapAPI/1.0',
							'Accept: application/json'
						],
						'timeout' => 5
					]
				]);

				$response = file_get_contents($url, false, $context);
				
				if ($response === false) {
					throw new \RuntimeException("HTTP request failed for {$source}");
				}

				$data = json_decode($response, true);
				
				if (!$data) {
					throw new \RuntimeException("Invalid JSON response from {$source}");
				}

				$price = null;

				if ($source === 'coingecko') {
					if (isset($data['binancecoin']['usd'])) {
						$price = (float)$data['binancecoin']['usd'];
					}
				} elseif ($source === 'binance') {
					if (isset($data['price'])) {
						$price = (float)$data['price'];
					}
				}

				if ($price === null) {
					throw new \RuntimeException("Price not found in {$source} response");
				}

				// Validate price is reasonable for BNB
				if ($price < 50 || $price > 5000) {
					throw new \RuntimeException("Unreasonable BNB price from {$source}: \${$price}");
				}

				// Convert to Chainlink-compatible format (8 decimals, string)
				$priceWith8Decimals = bcmul((string)$price, '100000000', 0);

				error_log("BNB/USD price from {$source}: \${$price} (raw: {$priceWith8Decimals})");

				return [
					'price' => $priceWith8Decimals,
					'decimals' => 8,
					'raw' => $response,
					'source' => $source,
					'original_price' => $price
				];

			} catch (Throwable $e) {
				$lastError = $e;
				error_log("BNB price source {$source} failed: " . $e->getMessage());
				continue;
			}
		}

		throw new \RuntimeException("All BNB price sources failed. Last error: " . $lastError->getMessage());
	}

	/** Return ABI-encoded dynamic string (length + bytes + right padding to 32 bytes), hex (no 0x). */
	private static function encodeStringWord(string $s): string
	{
		$bytes = bin2hex($s);                         // ASCII hex
		$len   = strlen($s);                          // byte length
		$lenWord = str_pad(dechex($len), 64, '0', STR_PAD_LEFT);
		$paddedLen = self::paddedLen($s);
		$data = str_pad($bytes, $paddedLen * 2, '0', STR_PAD_RIGHT);
		return $lenWord . $data;
	}

	/** Compute padded byte length for a dynamic string per ABI (multiple of 32). */
	private static function paddedLen(string $s): int
	{
		$L = strlen($s);
		$mod = $L % 32;
		return $mod === 0 ? $L : $L + (32 - $mod);
	}

    /** Accepts Space ID name (e.g., fr.boracle.bnb) or a direct 0x address; returns 0x address. */
    private function resolveSpaceIdToAddress(string $sidNameOrAddress): string
    {
        if (preg_match('/^0x[0-9a-fA-F]{40}$/', $sidNameOrAddress)) {
            return strtolower($sidNameOrAddress);
        }

        $node = $this->namehash($sidNameOrAddress);     // 0x + 64 hex
        $sidRegistry = '0xfFB52185b56603e0fd71De9de4F6f902f05EEA23'; // BSC Testnet SID Registry

        // resolver(bytes32)
        $dataResolver = $this->encodeResolverCall($node);
        $resolverHex  = $this->w3->ethCall($sidRegistry, $dataResolver);
        $resolver     = '0x' . substr(\LifeSwap\Hex::padLeft(\LifeSwap\Hex::strip0x($resolverHex), 64), 24, 40);
        if (!preg_match('/^0x[0-9a-f]{40}$/', strtolower($resolver))) {
            throw new \RuntimeException("SID resolver not found for {$sidNameOrAddress}");
        }

        // addr(bytes32)
        $dataAddr = $this->encodeAddrCall($node);
        $addrHex  = $this->w3->ethCall($resolver, $dataAddr);
        $addr     = '0x' . substr(\LifeSwap\Hex::padLeft(\LifeSwap\Hex::strip0x($addrHex), 64), 24, 40);
        if (!preg_match('/^0x[0-9a-f]{40}$/', strtolower($addr))) {
            throw new \RuntimeException("SID address not found for {$sidNameOrAddress}");
        }

        return strtolower($addr);
    }

    /* ===================== Encoding helpers ===================== */

    private function encodeExactInputSingle(
        string $tokenIn,
        string $tokenOut,
        int $fee,
        string $recipient,
        int $deadline,
        string $amountInHex,
        string $amountOutMinHex,
        string $sqrtPriceLimitX96Hex
    ): string {
        // function exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))
        $sig = 'exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))';
        $selector = '0x' . substr(Keccak::hash($sig, 256), 0, 8);

        $words = [
            $this->wordAddress($tokenIn),
            $this->wordAddress($tokenOut),
            $this->wordUint($fee),
            $this->wordAddress($recipient),
            $this->wordUint($deadline),
            $this->wordUintHex($amountInHex),
            $this->wordUintHex($amountOutMinHex),
            $this->wordUintHex($sqrtPriceLimitX96Hex)
        ];
        return $selector . implode('', $words);
    }

    private function encodeQuoteExactOutputSingle(
        string $tokenIn,
        string $tokenOut,
        string $amountOutHex, // uint256
        int $fee,             // uint24
        string $sqrtPriceLimitX96Hex // uint160
    ): string {
        // function quoteExactOutputSingle((address,address,uint256,uint24,uint160))
        $sig = 'quoteExactOutputSingle((address,address,uint256,uint24,uint160))';
        $selector = '0x' . substr(Keccak::hash($sig, 256), 0, 8);

        $words = [
            $this->wordAddress($tokenIn),
            $this->wordAddress($tokenOut),
            $this->wordUintHex($amountOutHex),
            $this->wordUint($fee),
            $this->wordUintHex($sqrtPriceLimitX96Hex),
        ];
        return $selector . implode('', $words);
    }

    private function encodeQuoteExactInputSingle(
        string $tokenIn,
        string $tokenOut,
        int $fee,             // uint24
        string $amountInHex,  // uint256
        string $sqrtPriceLimitX96Hex // uint160
    ): string {
        // function quoteExactInputSingle(address,address,uint24,uint256,uint160)
        $sig = 'quoteExactInputSingle(address,address,uint24,uint256,uint160)';
        $selector = '0x' . substr(Keccak::hash($sig, 256), 0, 8);

        $words = [
            $this->wordAddress($tokenIn),
            $this->wordAddress($tokenOut),
            $this->wordUint($fee),
            $this->wordUintHex($amountInHex),
            $this->wordUintHex($sqrtPriceLimitX96Hex),
        ];
        return $selector . implode('', $words);
    }

    private function selector(string $sig): string
    {
        return substr(Keccak::hash($sig, 256), 0, 8);
    }

    private function encodeResolverCall(string $nodeHex0x): string
    {
        $sel = $this->selector('resolver(bytes32)');
        $arg = $this->wordBytes32($nodeHex0x);
        return '0x' . $sel . $arg;
    }

    private function encodeAddrCall(string $nodeHex0x): string
    {
        $sel = $this->selector('addr(bytes32)');
        $arg = $this->wordBytes32($nodeHex0x);
        return '0x' . $sel . $arg;
    }

    private function wordAddress(string $addr): string
    {
        $h = Hex::padLeft(strtolower(Hex::strip0x($addr)), 20); // 20 bytes
        return Hex::padLeft($h, 32);
    }

    private function wordUint(int|string $v): string
    {
        $h = is_int($v) ? dechex($v) : Hex::strip0x(Hex::decStringToHex($v));
        return Hex::padLeft($h, 32);
    }

    private function wordUintHex(string $hex): string
    {
        $h = Hex::strip0x($hex);
        if ($h === '') $h = '0';
        return Hex::padLeft($h, 32);
    }

    private function wordBytes32(string $hexWithOrWithout0x): string
    {
        $h = Hex::strip0x($hexWithOrWithout0x);
        $h = str_pad($h, 64, '0', STR_PAD_LEFT);
        if ((strlen($h) % 2) !== 0) $h = '0' . $h;
        return $h;
    }

    /* ===================== Simple read helpers ===================== */

    private function callAllowance(string $token, string $owner, string $spender): string
    {
        $data = $this->weth->getData('allowance', strtolower($owner), strtolower($spender));
        return $this->w3->ethCall($token, $data);
    }

    private function callBalanceOf(string $token, string $owner): string
    {
        $data = $this->weth->getData('balanceOf', strtolower($owner));
        return $this->w3->ethCall($token, $data);
    }

    /* ===================== Misc helpers ===================== */

    private function lt(string $hexA, string $hexB): bool
    {
        $a = Hex::strip0x(strtolower($hexA)); if ($a==='') $a='0';
        $b = Hex::strip0x(strtolower($hexB)); if ($b==='') $b='0';
        $la = strlen($a); $lb = strlen($b);
        if ($la !== $lb) return $la < $lb;
        return strcmp($a, $b) < 0;
    }

   public function toWei(string $bnb): string
{
    // Normalize
    $bnb = trim($bnb);

    // Reject empty or formatted strings early
    if ($bnb === '') {
        throw new \InvalidArgumentException('toWei: amount cannot be empty');
    }
    // Only plain decimals allowed: digits with optional single dot and digits
    if (!preg_match('/^\d+(?:\.\d+)?$/', $bnb)) {
        throw new \InvalidArgumentException("toWei: amount must be a plain decimal like 0.1234; got '{$bnb}'");
    }

    if (!function_exists('bcscale')) {
        throw new \RuntimeException('BCMath extension is required');
    }
    bcscale(18);

    if (strpos($bnb, '.') === false) {
        // integer BNB
        $wei = bcmul($bnb, bcpow('10','18', 0), 0); // exact integer
    } else {
        // split integer and fractional parts
        [$i, $d] = explode('.', $bnb, 2);
        // limit to 18 decimals, right-pad to 18
        $d = substr($d, 0, 18);
        $d = rtrim($d, '0'); // (safe; can be '')
        $base = bcmul($i === '' ? '0' : $i, bcpow('10','18', 0), 0);
        $frac = $d === '' ? '0' : str_pad($d, 18, '0'); // exactly 18 digits or zero
        $wei  = bcadd($base, $frac, 0);
    }

    // Final guard: must be unsigned digits only
    if ($wei === '' || !preg_match('/^\d+$/', $wei)) {
        throw new \InvalidArgumentException("toWei: computed non-integer value '{$wei}'");
    }

    return Hex::decStringToHex($wei);
}

    /** ENS/SID-compatible namehash (EIP-137) */
    private function namehash(string $name): string
    {
        $node = str_repeat("\0", 32);
        $labels = array_reverse(array_filter(explode('.', strtolower(trim($name))), fn($x)=>$x!==''));
        foreach ($labels as $label) {
            $labelHash = hex2bin(Keccak::hash($label, 256));
            $node = hex2bin(Keccak::hash($node . $labelHash, 256));
        }
        return '0x' . bin2hex($node);
    }
}