//! Swap execution simulation module.
//!
//! This module provides functionality to simulate executing swaps through RPC requests
//! using state overwrites and historical blockchain data. It allows testing swap execution
//! against specific block states without actually performing on-chain transactions.

use std::{collections::HashMap, str::FromStr, sync::LazyLock};

use alloy::{
    primitives::{keccak256, map::AddressHashMap, Address, FixedBytes, U256},
    rpc::types::{state::AccountOverride, Block, TransactionRequest},
};
use miette::{miette, IntoDiagnostic, WrapErr};
use num_bigint::BigUint;
use tracing::info;
use tycho_execution::encoding::models::Solution;
use tycho_simulation::{
    evm::protocol::u256_num::{biguint_to_u256, u256_to_biguint},
    tycho_common::{
        traits::{AllowanceSlotDetector, BalanceSlotDetector},
        Bytes,
    },
    tycho_ethereum::entrypoint_tracer::{
        allowance_slot_detector::{AllowanceSlotDetectorConfig, EVMAllowanceSlotDetector},
        balance_slot_detector::{BalanceSlotDetectorConfig, EVMBalanceSlotDetector},
    },
};

use crate::rpc::RPCProvider;
pub const ROUTER_BYTECODE_JSON: &str =
    include_str!("../../evm/test/router/TychoRouter.runtime.json");
pub const EXECUTOR_ADDRESS: &str = "0xaE04CA7E9Ed79cBD988f6c536CE11C621166f41B";

// Include all executor bytecode files at compile time
const UNISWAP_V2_BYTECODE_JSON: &str =
    include_str!("../../evm/test/executors/UniswapV2.runtime.json");
const UNISWAP_V3_BYTECODE_JSON: &str =
    include_str!("../../evm/test/executors/UniswapV3.runtime.json");
const UNISWAP_V4_BYTECODE_JSON: &str =
    include_str!("../../evm/test/executors/UniswapV4.runtime.json");
const BALANCER_V2_BYTECODE_JSON: &str =
    include_str!("../../evm/test/executors/BalancerV2.runtime.json");
const BALANCER_V3_BYTECODE_JSON: &str =
    include_str!("../../evm/test/executors/BalancerV3.runtime.json");
const CURVE_BYTECODE_JSON: &str = include_str!("../../evm/test/executors/Curve.runtime.json");
const MAVERICK_V2_BYTECODE_JSON: &str =
    include_str!("../../evm/test/executors/MaverickV2.runtime.json");
const EKUBO_BYTECODE_JSON: &str = include_str!("../../evm/test/executors/Ekubo.runtime.json");

/// Mapping from protocol component patterns to executor bytecode JSON strings
static EXECUTOR_MAPPING: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
    let mut map = HashMap::new();
    map.insert("uniswap_v2", UNISWAP_V2_BYTECODE_JSON);
    map.insert("sushiswap", UNISWAP_V2_BYTECODE_JSON);
    map.insert("pancakeswap_v2", UNISWAP_V2_BYTECODE_JSON);
    map.insert("uniswap_v3", UNISWAP_V3_BYTECODE_JSON);
    map.insert("pancakeswap_v3", UNISWAP_V3_BYTECODE_JSON);
    map.insert("uniswap_v4", UNISWAP_V4_BYTECODE_JSON);
    map.insert("balancer_v2", BALANCER_V2_BYTECODE_JSON);
    map.insert("balancer_v3", BALANCER_V3_BYTECODE_JSON);
    map.insert("curve", CURVE_BYTECODE_JSON);
    map.insert("maverick_v2", MAVERICK_V2_BYTECODE_JSON);
    map.insert("ekubo", EKUBO_BYTECODE_JSON);
    map
});

/// Get executor bytecode JSON based on component ID
fn get_executor_bytecode_json(component_id: &str) -> miette::Result<&'static str> {
    for (pattern, executor_json) in EXECUTOR_MAPPING.iter() {
        if component_id.contains(pattern) {
            return Ok(executor_json);
        }
    }
    Err(miette!("Unknown component type '{}' - no matching executor found", component_id))
}

/// Load executor bytecode from embedded constants based on the protocol system
pub fn load_executor_bytecode(protocol_system: &str) -> miette::Result<Vec<u8>> {
    let executor_json = get_executor_bytecode_json(protocol_system)?;

    let json_value: serde_json::Value = serde_json::from_str(executor_json)
        .into_diagnostic()
        .wrap_err("Failed to parse executor JSON")?;

    let bytecode_str = json_value["runtimeBytecode"]
        .as_str()
        .ok_or_else(|| miette!("No bytecode field found in executor JSON"))?;

    // Remove 0x prefix if present
    let bytecode_hex =
        if let Some(stripped) = bytecode_str.strip_prefix("0x") { stripped } else { bytecode_str };

    hex::decode(bytecode_hex)
        .into_diagnostic()
        .wrap_err("Failed to decode executor bytecode from hex")
}

/// Calculate gas fees based on block base fee
fn calculate_gas_fees(block_header: &Block) -> miette::Result<(U256, U256)> {
    let base_fee = block_header
        .header
        .base_fee_per_gas
        .ok_or_else(|| miette::miette!("Block does not have base fee (pre-EIP-1559)"))?;
    // Set max_priority_fee_per_gas to a reasonable value (2 Gwei)
    let max_priority_fee_per_gas = U256::from(2_000_000_000u64); // 2 Gwei
                                                                 // Set max_fee_per_gas to base_fee * 2 + max_priority_fee_per_gas to handle fee fluctuations
    let max_fee_per_gas = U256::from(base_fee) * U256::from(2u64) + max_priority_fee_per_gas;

    info!(
        "Gas pricing: base_fee={}, max_priority_fee_per_gas={}, max_fee_per_gas={}",
        base_fee, max_priority_fee_per_gas, max_fee_per_gas
    );

    Ok((max_fee_per_gas, max_priority_fee_per_gas))
}

/// Calculate storage slot for Solidity mapping.
///
/// The solidity code:
/// keccak256(abi.encodePacked(bytes32(key), bytes32(slot)))
pub fn calculate_executor_storage_slot(key: Address) -> FixedBytes<32> {
    // Convert key (20 bytes) to 32-byte left-padded array (uint256)
    let mut key_bytes = [0u8; 32];
    key_bytes[12..].copy_from_slice(key.as_slice());

    // The base of the executor storage slot is 1, since there is only one
    // variable that is initialized before it (which is _roles in AccessControl.sol).
    // In this case, _roles gets slot 0.
    // The slots are given in order to the parent contracts' variables first and foremost.
    let slot = U256::from(1);

    // Convert U256 slot to 32-byte big-endian array
    let slot_bytes = slot.to_be_bytes::<32>();

    // Concatenate key_bytes + slot_bytes, then keccak hash
    let mut buf = [0u8; 64];
    buf[..32].copy_from_slice(&key_bytes);
    buf[32..].copy_from_slice(&slot_bytes);
    keccak256(buf)
}

/// Sets up state overwrites for the Tycho router and its associated executor.
///
/// This method prepares the router for simulation by:
/// 1. Overriding the router's bytecode with the embedded runtime bytecode
/// 2. Copying executor approval storage from the current block to maintain permissions
/// 3. Overriding the executor's bytecode based on the protocol system
///
/// # Arguments
/// * `router_address` - The address of the Tycho router contract
/// * `protocol_system` - The protocol system identifier (e.g., "uniswap_v2", "vm:balancer_v2")
///
/// # Returns
/// A HashMap containing account overwrites for both the router and executor addresses.
/// The router override includes bytecode and executor approval storage.
/// The executor override includes the appropriate bytecode for the protocol.
///
/// # Errors
/// Returns an error if:
/// - Router bytecode JSON parsing fails
/// - Executor address parsing fails
/// - Storage slot fetching fails
/// - Executor bytecode loading fails
pub async fn setup_router_overwrites(
    router_address: Address,
    protocol_system: &str,
) -> miette::Result<AddressHashMap<AccountOverride>> {
    let json_value: serde_json::Value = serde_json::from_str(ROUTER_BYTECODE_JSON)
        .into_diagnostic()
        .wrap_err("Failed to parse router JSON")?;

    let bytecode_str = json_value["runtimeBytecode"]
        .as_str()
        .ok_or_else(|| miette::miette!("No runtimeBytecode field found in router JSON"))?;

    // Remove 0x prefix if present
    let bytecode_hex =
        if let Some(stripped) = bytecode_str.strip_prefix("0x") { stripped } else { bytecode_str };

    let router_bytecode = hex::decode(bytecode_hex)
        .into_diagnostic()
        .wrap_err("Failed to decode router bytecode from hex")?;

    // Start with the router bytecode override
    let mut state_overwrites = AddressHashMap::default();
    let mut tycho_router_override = AccountOverride::default().with_code(router_bytecode);

    // Find executor address approval storage slot
    let executor_address = Address::from_str(EXECUTOR_ADDRESS).into_diagnostic()?;
    let storage_slot = calculate_executor_storage_slot(executor_address);

    // The executors mapping starts at storage value 1
    let storage_value = FixedBytes::<32>::from(U256::ONE);

    tycho_router_override =
        tycho_router_override.with_state_diff(vec![(storage_slot, storage_value)]);

    state_overwrites.insert(router_address, tycho_router_override);

    // Add bytecode overwrite for the executor
    let executor_bytecode = load_executor_bytecode(protocol_system)?;
    state_overwrites
        .insert(executor_address, AccountOverride::default().with_code(executor_bytecode.to_vec()));
    Ok(state_overwrites)
}

/// Sets up state overwrites for user accounts and tokens required for swap simulation.
///
/// This method prepares the user environment for historical block simulation by:
/// 1. Providing the user with sufficient ETH balance (100 ETH) for gas payments
/// 2. For ETH swaps: Adding the swap amount to the user's ETH balance
/// 3. For ERC20 swaps: Overriding token balance and allowance storage slots to ensure:
///    - User has sufficient tokens for the swap
///    - Router has unlimited allowance to spend user's tokens
///
/// The function uses EVM storage slot detection to find the correct storage locations
/// for token balances and allowances, then applies state overwrites to simulate the
/// required pre-conditions without executing actual token transfers.
///
/// # Arguments
/// * `solution` - The encoded swap solution containing token and amount information
/// * `transaction` - The transaction details for determining router address
/// * `user_address` - The address of the user performing the swap
/// * `rpc_url` - RPC endpoint URL for storage slot detection
/// * `block` - The historical block context for storage queries
///
/// # Returns
/// A HashMap containing account overwrites for:
/// - User account: ETH balance override
/// - Token contract: Balance and allowance storage slot overwrites (for ERC20 swaps)
///
/// # Errors
/// Returns an error if:
/// - Storage slot detection fails for balance or allowance
/// - Token address parsing fails
/// - RPC queries for storage detection fail
async fn setup_user_overwrites(
    solution: &Solution,
    transaction: &tycho_execution::encoding::models::Transaction,
    user_address: Address,
    rpc_url: String,
    block: &Block,
) -> miette::Result<AddressHashMap<AccountOverride>> {
    let mut overwrites = AddressHashMap::default();
    let token_address = Address::from_slice(&solution.given_token[..20]);
    // If given token is ETH, add the given amount to the balance + 100 ETH for gas
    if solution.given_token == Bytes::zero(20) {
        let eth_balance = biguint_to_u256(&solution.given_amount) +
            U256::from_str("100000000000000000000").unwrap(); // given_amount + 100 ETH for gas
        overwrites.insert(user_address, AccountOverride::default().with_balance(eth_balance));
    // if the given token is not ETH, do balance and allowance slots overwrites
    } else {
        let detector = EVMBalanceSlotDetector::new(BalanceSlotDetectorConfig {
            rpc_url: rpc_url.clone(),
            ..Default::default()
        })
        .into_diagnostic()?;

        let results = detector
            .detect_balance_slots(
                std::slice::from_ref(&solution.given_token),
                (**user_address).into(),
                (*block.header.hash).into(),
            )
            .await;

        let (balance_storage_addr, balance_slot) =
            if let Some(Ok((storage_addr, slot))) = results.get(&solution.given_token.clone()) {
                (storage_addr, slot)
            } else {
                return Err(miette!("Couldn't find balance storage slot for token {token_address}"));
            };

        let detector = EVMAllowanceSlotDetector::new(AllowanceSlotDetectorConfig {
            rpc_url,
            ..Default::default()
        })
        .into_diagnostic()?;

        let results = detector
            .detect_allowance_slots(
                std::slice::from_ref(&solution.given_token),
                (**user_address).into(),
                transaction.to.clone(), // tycho router
                (*block.header.hash).into(),
            )
            .await;

        let (allowance_storage_addr, allowance_slot) = if let Some(Ok((storage_addr, slot))) =
            results.get(&solution.given_token.clone())
        {
            (storage_addr, slot)
        } else {
            return Err(miette!("Couldn't find allowance storage slot for token {token_address}"));
        };

        // Use the exact given amount for balance and allowance (no buffer, no max)
        let token_balance = biguint_to_u256(&solution.given_amount);
        let token_allowance = biguint_to_u256(&solution.given_amount);

        let balance_storage_address = Address::from_slice(&balance_storage_addr[..20]);
        let allowance_storage_address = Address::from_slice(&allowance_storage_addr[..20]);

        // Apply balance and allowance overrides
        // If both storage addresses are the same, combine them into one override
        if balance_storage_address == allowance_storage_address {
            overwrites.insert(
                balance_storage_address,
                AccountOverride::default().with_state_diff(vec![
                    (
                        alloy::primitives::B256::from_slice(balance_slot),
                        alloy::primitives::B256::from_slice(&token_balance.to_be_bytes::<32>()),
                    ),
                    (
                        alloy::primitives::B256::from_slice(allowance_slot),
                        alloy::primitives::B256::from_slice(&token_allowance.to_be_bytes::<32>()),
                    ),
                ]),
            );
        } else {
            // Different storage addresses, apply separately
            overwrites.insert(
                balance_storage_address,
                AccountOverride::default().with_state_diff(vec![(
                    alloy::primitives::B256::from_slice(balance_slot),
                    alloy::primitives::B256::from_slice(&token_balance.to_be_bytes::<32>()),
                )]),
            );
            overwrites.insert(
                allowance_storage_address,
                AccountOverride::default().with_state_diff(vec![(
                    alloy::primitives::B256::from_slice(allowance_slot),
                    alloy::primitives::B256::from_slice(&token_allowance.to_be_bytes::<32>()),
                )]),
            );
        }

        // Add 1 ETH for gas for non-ETH token swaps
        let eth_balance = U256::from_str("100000000000000000000").unwrap(); // 100 ETH for gas
        overwrites.insert(user_address, AccountOverride::default().with_balance(eth_balance));
    }
    Ok(overwrites)
}

/// Simulate a trade using eth_call for historical blocks
pub async fn simulate_trade_with_eth_call(
    rpc_provider: &RPCProvider,
    transaction: &tycho_execution::encoding::models::Transaction,
    solution: &Solution,
    block: &Block,
) -> miette::Result<BigUint> {
    let first_swap = solution.swaps.first().unwrap();
    let protocol_system = &first_swap.component.protocol_system;

    let user_address = Address::from_slice(&solution.sender[..20]);
    let (max_fee_per_gas, max_priority_fee_per_gas) = calculate_gas_fees(block)?;
    // Convert main transaction to alloy TransactionRequest
    let execution_tx = TransactionRequest::default()
        .to(Address::from_slice(&transaction.to[..20]))
        .input(transaction.data.clone().into())
        .value(U256::from_str(&transaction.value.to_string()).unwrap_or_default())
        .from(user_address)
        .max_fee_per_gas(
            max_fee_per_gas
                .try_into()
                .unwrap_or(u128::MAX),
        )
        .max_priority_fee_per_gas(
            max_priority_fee_per_gas
                .try_into()
                .unwrap_or(u128::MAX),
        );
    let tycho_router_address = Address::from_slice(&transaction.to[..20]);

    let router_overwrites = setup_router_overwrites(tycho_router_address, protocol_system)
        .await
        .wrap_err("Failed to create router override")?;

    let mut user_overwrites = setup_user_overwrites(
        solution,
        transaction,
        user_address,
        rpc_provider.url.to_string(),
        block,
    )
    .await?;

    // Merge router overwrites with user overwrites
    user_overwrites.extend(router_overwrites);

    let execution_amount_out = rpc_provider
        .simulate_transactions_with_tracing(execution_tx, block.number(), user_overwrites)
        .await
        .map_err(|e| {
            info!("Execution transaction failed with error: {}", e);
            e
        })
        .wrap_err("Execution simulation failed")?;

    Ok(u256_to_biguint(execution_amount_out))
}
