import { ethers } from 'ethers';
import { Token } from '@uniswap/sdk-core';
import fs from 'fs';

//
// TOKEN DEFINITIONS
//
const ChainId = {
    MAINNET: 1
};

const USDC_TOKEN = new Token(
    ChainId.MAINNET,
    '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
    6,
    'USDC',
    'USDC'
);

const WETH_TOKEN = new Token(
    ChainId.MAINNET,
    '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
    18,
    'WETH',
    'WETH'
);

const USDT_TOKEN= new Token(
    ChainId.MAINNET,
    '0xdAC17F958D2ee523a2206206994597C13D831ec7',
    6,
    'USDT',
    'USDT'
);


//
// POOL DEFINITION
//

const tokenA = USDC_TOKEN
const tokenB = WETH_TOKEN
// Pool ID to fetch pool key from
// const POOL_ID = '0x8aa4e11cbdf30eedc92100f4c8a31ff748e201d44712cc8c90d189edaa8e4e47'; // USDT-USDC 0.0010%
// const POOL_ID = '0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d'; // USDC-WETH 0.30%
const POOL_ID = '0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d'; // USDC-WETH 0.05%


// Configuration
const QUOTER_ADDRESS = '0x52f0e24d1c21c8a0cb1e5a5dd6198556bd9e1203';
const POSITION_MANAGER_ADDRESS = '0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e';


// Providers
const anvilProvider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:8545');

// Quoter contract ABI
const QUOTER_ABI = [
    {
        inputs: [
            {
                components: [
                    {
                        components: [
                            { name: 'currency0', type: 'address' },
                            { name: 'currency1', type: 'address' },
                            { name: 'fee', type: 'uint24' },
                            { name: 'tickSpacing', type: 'int24' },
                            { name: 'hooks', type: 'address' }
                        ],
                        name: 'poolKey',
                        type: 'tuple'
                    },
                    { name: 'zeroForOne', type: 'bool' },
                    { name: 'exactAmount', type: 'uint128' },
                    { name: 'hookData', type: 'bytes' }
                ],
                name: 'params',
                type: 'tuple'
            }
        ],
        name: 'quoteExactInputSingle',
        outputs: [
            {
                components: [
                    { name: 'amountOut', type: 'uint256' }
                ],
                name: 'result',
                type: 'tuple'
            }
        ],
        stateMutability: 'view',
        type: 'function'
    }
];
// StateView ABI for getSlot0 and getLiquidity
const STATE_VIEW_ABI = [
    {
        inputs: [
            // { name: 'manager', type: 'address' },
            { name: 'poolId', type: 'bytes32' }
        ],
        name: 'getSlot0',
        outputs: [
            { name: 'sqrtPriceX96', type: 'uint160' },
            { name: 'tick', type: 'int24' },
            { name: 'protocolFee', type: 'uint24' },
            { name: 'lpFee', type: 'uint24' }
        ],
        stateMutability: 'view',
        type: 'function'
    },
];

// StateView contract address for reading pool state
const STATE_VIEW_ADDRESS = '0x7ffe42c4a5deea5b0fec41c94c136cf115597227';

// Position Manager ABI
const POSITION_MANAGER_ABI = [
    {
        inputs: [{ name: 'poolId', type: 'bytes25' }],
        name: 'poolKeys',
        outputs: [
            {
                components: [
                    { name: 'currency0', type: 'address' },
                    { name: 'currency1', type: 'address' },
                    { name: 'fee', type: 'uint24' },
                    { name: 'tickSpacing', type: 'int24' },
                    { name: 'hooks', type: 'address' }
                ],
                name: 'poolKey',
                type: 'tuple'
            }
        ],
        stateMutability: 'view',
        type: 'function'
    }
];

// Function to get pool key from Position Manager (uses mainnet)
async function getPoolKey() {
    try {
        const positionManager = new ethers.Contract(
            POSITION_MANAGER_ADDRESS,
            POSITION_MANAGER_ABI,
            anvilProvider
        );

        // Extract first 25 bytes (50 hex chars + 0x prefix = 52 chars)
        const poolIdBytes25 = POOL_ID.slice(0, 52);

        const poolKey = await positionManager.poolKeys(poolIdBytes25);

        return {
            currency0: poolKey.currency0,
            currency1: poolKey.currency1,
            fee: poolKey.fee,
            tickSpacing: poolKey.tickSpacing,
            hooks: poolKey.hooks
        };
    } catch (error) {
        console.error('Error fetching pool key:', error.message);
        throw error;
    }
}

// Helper function to format BigNumber for display (use only for console output)
function formatBigNumberForDisplay(bigNumber, decimals) {
    const formatted = ethers.utils.formatUnits(bigNumber, decimals);
    return formatted;
}

// Helper function to convert BigNumber to fixed decimal string without scientific notation
function bigNumberToFixed(bigNumber, decimals) {
    const str = ethers.utils.formatUnits(bigNumber, decimals);
    // Ensure no scientific notation
    if (str.includes('e')) {
        const num = parseFloat(str);
        return num.toFixed(decimals);
    }
    return str;
}

// Convert sqrtPriceX96 to price as a decimal string (maintains precision)
// Returns price representing token1/token0
function sqrtPriceX96ToPrice(sqrtPriceX96, decimals0, decimals1) {
    const sqrtPrice = ethers.BigNumber.from(sqrtPriceX96);

    console.log('DEBUG sqrtPriceX96ToPrice:');
    console.log('  sqrtPriceX96:', sqrtPriceX96.toString());
    console.log('  decimals0:', decimals0, 'decimals1:', decimals1);

    if (sqrtPrice.isZero()) {
        throw new Error('sqrtPriceX96 cannot be zero');
    }

    // Calculate price = (sqrtPriceX96 / 2^96)^2 * 10^(decimals0 - decimals1)
    // Using BigNumber arithmetic throughout to maintain precision

    const Q96 = ethers.BigNumber.from(2).pow(96);

    // price = sqrtPrice^2 / 2^192 * 10^(decimals0 - decimals1)
    // To maintain precision, we need to be careful about the order of operations

    // First square the sqrtPrice
    const sqrtPriceSquared = sqrtPrice.mul(sqrtPrice);
    console.log('  sqrtPriceSquared:', sqrtPriceSquared.toString());

    // Calculate 2^192
    const Q192 = Q96.mul(Q96);
    console.log('  Q192:', Q192.toString());

    // Adjust for decimals: if decimals0 > decimals1, we multiply, else divide
    const decimalDiff = decimals0 - decimals1;
    console.log('  decimalDiff:', decimalDiff);

    // We need to calculate: sqrtPriceSquared * 10^decimalDiff / Q192
    // Use a large precision scale to avoid truncation
    // We'll use 30 decimals to ensure we have enough precision even for extreme price ratios

    const PRECISION_DECIMALS = 30;
    const precisionScale = ethers.BigNumber.from(10).pow(PRECISION_DECIMALS);
    let price;

    if (decimalDiff >= 0) {
        const decimalAdjustment = ethers.BigNumber.from(10).pow(decimalDiff);
        const numerator = sqrtPriceSquared.mul(decimalAdjustment).mul(precisionScale);
        console.log('  numerator:', numerator.toString());
        price = numerator.div(Q192);
    } else {
        // When decimalDiff is negative, multiply denominator instead of dividing numerator
        // This avoids precision loss
        const decimalAdjustment = ethers.BigNumber.from(10).pow(-decimalDiff);
        const numerator = sqrtPriceSquared.mul(precisionScale);
        console.log('  numerator:', numerator.toString());
        const denominator = Q192.mul(decimalAdjustment);
        console.log('  denominator:', denominator.toString());
        price = numerator.div(denominator);
    }

    console.log('  price (raw):', price.toString());

    const priceFormatted = ethers.utils.formatUnits(price, PRECISION_DECIMALS);
    console.log('  price (formatted):', priceFormatted);

    return priceFormatted;
}

// Helper to calculate inverse price from a decimal string
function inversePrice(priceStr) {
    const priceNum = parseFloat(priceStr);
    if (priceNum === 0 || !isFinite(priceNum)) {
        console.warn('Warning: Cannot calculate inverse of zero or invalid price');
        return '0';
    }

    // Calculate 1/price using standard division
    const inverse = 1.0 / priceNum;

    // If result is very large, format as integer-like
    // If result is normal decimal, format with precision
    if (inverse > 1e10) {
        return inverse.toExponential(18).replace(/e\+?/, 'e');
    }

    // Format with high precision, removing trailing zeros
    let formatted = inverse.toFixed(18);
    // Remove trailing zeros but keep at least one decimal place
    formatted = formatted.replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '.0');
    return formatted;
}

// Get pool price using StateView
async function getPoolPriceWithSDK(blockNumber) {
    try {
        // Create StateView contract with ethers
        const stateViewContract = new ethers.Contract(
            STATE_VIEW_ADDRESS,
            STATE_VIEW_ABI,
            anvilProvider
        );

        // Get slot0 data
        const slot0 = await stateViewContract.getSlot0(POOL_ID, {
            blockTag: blockNumber
        });

        // Convert sqrtPriceX96 to price as BigNumber (maintains precision)
        console.log('\n=== Pool State (Debug) ===');
        console.log('Block:', blockNumber);
        console.log('sqrtPriceX96:', slot0.sqrtPriceX96.toString());
        console.log('Tick:', slot0.tick.toString());
        console.log('Token A decimals:', tokenA.decimals);
        console.log('Token B decimals:', tokenB.decimals);

        // Calculate price: token0/token1 = USDC/WETH  
        // Using swapped decimals gives us the correct direction
        const token0PerToken1 = sqrtPriceX96ToPrice(slot0.sqrtPriceX96, tokenB.decimals, tokenA.decimals);
        console.log('Price USDC per WETH:', token0PerToken1);

        // Calculate the reciprocal to get WETH per USDC
        const token0PerToken1Num = parseFloat(token0PerToken1);
        const token1PerToken0 = (1.0 / token0PerToken1Num).toFixed(18).replace(/\.?0+$/, '');
        console.log('Price WETH per USDC (reciprocal):', token1PerToken0);

        console.log('\n=== Pool State ===');
        console.log('Block:', blockNumber);
        console.log('sqrtPriceX96:', slot0.sqrtPriceX96.toString());
        console.log('Tick:', slot0.tick.toString());
        console.log(`Price calculated as: token1/token0 where token0=${tokenA.symbol}(${tokenA.decimals}d), token1=${tokenB.symbol}(${tokenB.decimals}d)`);
        console.log(`${tokenB.symbol} per ${tokenA.symbol}:`, token1PerToken0);
        console.log(`${tokenA.symbol} per ${tokenB.symbol}:`, token0PerToken1);
        console.log('LP Fee:', slot0.lpFee, `(${(slot0.lpFee / 10000).toFixed(2)}%)`);

        // Sanity check with actual swap data
        console.log('\nSanity check: If swapping 1 USDC should get ~0.000256 WETH based on your data');

        return {
            sqrtPriceX96: slot0.sqrtPriceX96.toString(),
            tick: slot0.tick.toString(),
            priceStr: token1PerToken0,        // WETH per USDC (tokenB per tokenA)
            inversePriceStr: token0PerToken1, // USDC per WETH (tokenA per tokenB) 
            protocolFee: slot0.protocolFee,
            lpFee: slot0.lpFee
        };
    } catch (error) {
        console.error('Error getting pool price:', error.message);
        throw error;
    }
}


// Get quote from historical block (10 blocks back) with multiple amount testing
async function getQuoteFromHistoricalBlock(poolKey, targetBlock) {
    try {

        // Get starting pool price using SDK
        try {
            await getPoolPriceWithSDK(targetBlock);
        } catch (error) {
            console.error('Error getting pool price with SDK:', error.message);
        }

        console.log('\n=== Testing Multiple Input Amounts ===');
        console.log('Testing amounts from 1 USDC, increasing by 30% each iteration (limited to 5 iterations)');
        console.log('Testing both directions: USDC→USDT and USDT→USDC\n');

        // Create a new provider instance that queries at specific block
        const quoterContract = new ethers.Contract(QUOTER_ADDRESS, QUOTER_ABI, anvilProvider);

        const resultsForward = [];  // USDC→USDT
        const resultsReverse = [];  // USDT→USDC

        // Start with 1 token as BigNumber
        let currentAmountBN = ethers.utils.parseUnits("1", tokenA.decimals);
        const multiplierNumerator = 13; // 130% = 13/10
        const multiplierDenominator = 10;
        const maxIterations = 200;
        const maxAmount = ethers.utils.parseUnits("1000000", tokenA.decimals); // 10 million limit

        // Test USDC→USDT (forward direction)
        console.log('\n=== USDC → USDT (Forward Direction) ===');
        for (let i = 0; i < maxIterations; i++) {
            // Check if currentAmountBN exceeds 10 million
            if (currentAmountBN.gt(maxAmount)) {
                console.log(`\nReached maximum amount limit of 10 million USDC. Stopping iterations.`);
                break;
            }

            const currentAmountFormatted = formatBigNumberForDisplay(currentAmountBN, tokenA.decimals);
            console.log(`\n--- Iteration ${i + 1}: ${currentAmountFormatted} USDC ---`);

            const amountIn = currentAmountBN;

            // Make the call at the specific block using overrides
            const quotedAmountOut = await quoterContract.callStatic.quoteExactInputSingle({
                poolKey: poolKey,
                zeroForOne: tokenA.address.toLowerCase() > tokenB.address.toLowerCase(),
                exactAmount: amountIn.toString(),
                hookData: '0x00',
            }, {
                blockTag: targetBlock // Query at specific historical block
            });

            console.log('in/out', amountIn, quotedAmountOut.amountIn, quotedAmountOut.amountOut);
            const actualAmountOut = ethers.BigNumber.from(quotedAmountOut.amountOut);

            // Calculate effective exchange rate as BigNumber (with extra precision)
            // effectiveRate = actualAmountOut / amountIn
            // Scale to show tokenB per 1 tokenA (use tokenB decimals for result)
            const scaleFactor = ethers.BigNumber.from(10).pow(tokenB.decimals);
            const effectiveRateBN = actualAmountOut.mul(scaleFactor).div(amountIn);

            // Log results for this amount (format only for display)
            console.log(`Amount In: ${currentAmountFormatted} ${tokenA.symbol}`);
            console.log(`Amount Out: ${formatBigNumberForDisplay(actualAmountOut, tokenB.decimals)} ${tokenB.symbol}`);

            // Store result with full precision (as strings)
            resultsForward.push({
                iteration: i + 1,
                amountInBN: amountIn.toString(),
                amountOutBN: actualAmountOut.toString(),
                effectiveRateBN: effectiveRateBN.toString(),
            });

            // Increase amount by 30% for next iteration using BigNumber math
            currentAmountBN = currentAmountBN.mul(multiplierNumerator).div(multiplierDenominator);
        }

        // Summary
        console.log('\n\n=== Summary of Results ===');

        console.log(`--- Forward Direction (${tokenA.symbol} → ${tokenB.symbol}) ---`);
        console.log('\nAmount In → Amount Out (Rate) [Slippage]');
        resultsForward.forEach(r => {
            const amountIn = formatBigNumberForDisplay(ethers.BigNumber.from(r.amountInBN), tokenA.decimals);
            const amountOut = formatBigNumberForDisplay(ethers.BigNumber.from(r.amountOutBN), tokenB.decimals);
            const rate = formatBigNumberForDisplay(ethers.BigNumber.from(r.effectiveRateBN), tokenB.decimals);
            console.log(`${amountIn} ${tokenA.symbol} → ${amountOut} ${tokenB.symbol} (${rate})`);
        });

        return {
            blockNumber: targetBlock,
            forward: resultsForward,
            reverse: resultsReverse
        };
    } catch (error) {
        console.error('Error getting historical quote:', error.message);
        throw error;
    }
}

// Function to write results to CSV with full precision
function writeResultsToCSV(blockNumber, priceData, quoteResults) {
    const filename = `swap_results_block_${blockNumber}.csv`;

    // Create CSV header
    let csvContent = `block,${tokenB.symbol} per ${tokenA.symbol},${tokenA.symbol} per ${tokenB.symbol},Amount In,Amount Out,Effective Rate\n`;

    // Get max number of iterations
    const maxIterations = quoteResults.forward.length;

    // Add data rows
    for (let i = 0; i < maxIterations; i++) {
        const forwardResult = quoteResults.forward[i];

        // Use full precision for prices (no scientific notation)
        const priceStr = priceData.priceStr;
        const inversePriceStr = priceData.inversePriceStr;
        csvContent += `${blockNumber},${priceStr},${inversePriceStr},`;

        // Forward direction data with full precision
        const amountInFormatted = bigNumberToFixed(ethers.BigNumber.from(forwardResult.amountInBN), tokenA.decimals);
        const amountOutFormatted = bigNumberToFixed(ethers.BigNumber.from(forwardResult.amountOutBN), tokenB.decimals);
        const effectiveRate = bigNumberToFixed(ethers.BigNumber.from(forwardResult.effectiveRateBN), tokenB.decimals);

        csvContent += `${amountInFormatted},${amountOutFormatted},${effectiveRate}\n`;
    }
    
    // Write to file
    fs.writeFileSync(filename, csvContent);
    console.log(`\n✅ Results written to ${filename}`);
}

// Main function
async function main() {
    console.log('=== Uniswap V4 Quote and Gas Estimation ===\n');
    // Get current block
    const currentBlock = await anvilProvider.getBlockNumber();
    const targetBlock = currentBlock - 10;


    // Fetch pool key from Position Manager
    let poolKey;
    try {
        poolKey = await getPoolKey();
    } catch (error) {
        console.error('Failed to fetch pool key. Exiting.');
        return;
    }

    // Get pool price data
    let poolPriceData;
    try {
        poolPriceData = await getPoolPriceWithSDK(targetBlock);
    } catch (error) {
        console.error('Failed to get pool price data:', error.message);
        return;
    }

    // Get quotes for both directions
    try {
        const quoteResults = await getQuoteFromHistoricalBlock(poolKey, targetBlock);
        console.log('✅ Historical quote successful!');
        
        // Write results to CSV
        writeResultsToCSV(targetBlock, poolPriceData, quoteResults);
    } catch (error) {
        console.error('❌ Failed to get historical quote:', error.message);
    }
}

// Run the main function
main().catch(console.error);
