use crate::traces::identifier::SignaturesIdentifier;
use alloy_consensus::{SidecarBuilder, SimpleCoder};
use alloy_dyn_abi::ErrorExt;
use alloy_ens::NameOrAddress;
use alloy_json_abi::Function;
use alloy_network::{Network, ReceiptResponse, TransactionBuilder};
use alloy_primitives::{Address, B256, Bytes, TxHash, TxKind, U64, U256, hex};
use alloy_provider::{PendingTransactionBuilder, Provider};
use alloy_rpc_types::{AccessList, Authorization, TransactionInputKind};
use alloy_signer::Signer;
use alloy_transport::TransportError;
use clap::Args;
use eyre::{Result, WrapErr};
use foundry_cli::{
    opts::{CliAuthorizationList, EthereumOpts, TempoOpts, TransactionOpts},
    utils::{self, parse_function_args},
};
use foundry_common::{
    FoundryTransactionBuilder, TransactionReceiptWithRevertReason,
    fmt::*,
    get_pretty_receipt_w_reason_attr,
    provider::fee::{estimate_eip1559_fees, resolve_broadcast_eip1559_fees},
    shell,
};
use foundry_config::{Chain, Config, Eip1559FeeEstimatePreset};
use foundry_wallets::{BrowserWalletOpts, TempoAccountsWallet, WalletOpts, WalletSigner};
use itertools::Itertools;
use serde_json::value::RawValue;
use std::{fmt::Write, marker::PhantomData, str::FromStr, time::Duration};

#[derive(Debug, Clone, Args)]
pub struct SendTxOpts {
    /// Only print the transaction hash and exit immediately.
    #[arg(id = "async", long = "async", alias = "cast-async", env = "CAST_ASYNC")]
    pub cast_async: bool,

    /// Wait for transaction receipt synchronously instead of polling.
    /// Note: uses `eth_sendTransactionSync` or `eth_sendRawTransactionSync`, which may not be
    /// supported by all clients.
    #[arg(long, conflicts_with = "async")]
    pub sync: bool,

    /// The number of confirmations until the receipt is fetched.
    #[arg(long, default_value = "1")]
    pub confirmations: u64,

    /// Timeout for sending the transaction.
    #[arg(long, env = "ETH_TIMEOUT")]
    pub timeout: Option<u64>,

    /// Polling interval for transaction receipts (in seconds).
    #[arg(long, alias = "poll-interval", env = "ETH_POLL_INTERVAL")]
    pub poll_interval: Option<u64>,

    /// Ethereum options
    #[command(flatten)]
    pub eth: EthereumOpts,

    /// Browser wallet options
    #[command(flatten)]
    pub browser: BrowserWalletOpts,
}

/// Transaction options shared across cast commands that submit on-chain transactions.
#[derive(Debug, Clone, Args)]
#[command(next_help_heading = "Transaction options")]
pub struct TxParams {
    /// Gas limit for the transaction.
    #[arg(long, env = "ETH_GAS_LIMIT")]
    pub gas_limit: Option<U256>,

    /// Gas price for legacy transactions, or max fee per gas for EIP1559 transactions.
    #[arg(long, env = "ETH_GAS_PRICE")]
    pub gas_price: Option<U256>,

    /// Max priority fee per gas for EIP1559 transactions.
    #[arg(long, env = "ETH_PRIORITY_GAS_PRICE")]
    pub priority_gas_price: Option<U256>,

    /// Nonce for the transaction.
    #[arg(long)]
    pub nonce: Option<U64>,

    #[command(flatten)]
    pub tempo: TempoOpts,
}

impl TxParams {
    pub(crate) fn apply<N: Network>(&self, tx: &mut N::TransactionRequest, legacy: bool)
    where
        N::TransactionRequest: FoundryTransactionBuilder<N>,
    {
        if let Some(gas_limit) = self.gas_limit {
            tx.set_gas_limit(gas_limit.to());
        }

        if let Some(gas_price) = self.gas_price {
            if legacy {
                tx.set_gas_price(gas_price.to());
            } else {
                tx.set_max_fee_per_gas(gas_price.to());
            }
        }

        if !legacy && let Some(priority_fee) = self.priority_gas_price {
            tx.set_max_priority_fee_per_gas(priority_fee.to());
        }

        self.tempo.apply::<N>(tx, self.nonce.map(|n| n.to()));
    }
}

/// Different sender kinds used by [`CastTxBuilder`].
pub enum SenderKind<'a> {
    /// An address without signer. Used for read-only calls and transactions sent through unlocked
    /// accounts.
    Address(Address),
    /// A reference to a signer.
    Signer(&'a WalletSigner),
    /// An owned signer.
    OwnedSigner(Box<WalletSigner>),
}

impl SenderKind<'_> {
    /// Resolves the name to an Ethereum Address.
    pub fn address(&self) -> Address {
        match self {
            Self::Address(addr) => *addr,
            Self::Signer(signer) => signer.address(),
            Self::OwnedSigner(signer) => signer.address(),
        }
    }

    /// Resolves the sender from the wallet options.
    ///
    /// This function prefers the `from` field and may return a different address from the
    /// configured signer
    /// If from is specified, returns it
    /// If from is not specified, but there is a signer configured, returns the signer's address
    /// If from is not specified and there is no signer configured, returns zero address
    pub async fn from_wallet_opts(mut opts: WalletOpts) -> Result<Self> {
        let from = opts.from.take();
        let (signer, tempo_wallet) = opts.maybe_signer().await?;
        if let Some(signer) = signer {
            Ok(Self::OwnedSigner(Box::new(signer)))
        } else if let Some(tempo_wallet) = tempo_wallet {
            Ok(tempo_wallet.account().into())
        } else if let Some(from) = from {
            Ok(from.into())
        } else {
            Ok(Address::ZERO.into())
        }
    }

    /// Returns the signer if available.
    pub fn as_signer(&self) -> Option<&WalletSigner> {
        match self {
            Self::Signer(signer) => Some(signer),
            Self::OwnedSigner(signer) => Some(signer.as_ref()),
            _ => None,
        }
    }
}

impl From<Address> for SenderKind<'_> {
    fn from(addr: Address) -> Self {
        Self::Address(addr)
    }
}

impl<'a> From<&'a WalletSigner> for SenderKind<'a> {
    fn from(signer: &'a WalletSigner) -> Self {
        Self::Signer(signer)
    }
}

impl From<WalletSigner> for SenderKind<'_> {
    fn from(signer: WalletSigner) -> Self {
        Self::OwnedSigner(Box::new(signer))
    }
}

/// Prevents a misconfigured hwlib from sending a transaction that defies user-specified --from
pub fn validate_from_address(
    specified_from: Option<Address>,
    signer_address: Address,
) -> Result<()> {
    if let Some(specified_from) = specified_from
        && specified_from != signer_address
    {
        eyre::bail!(
                "\
The specified sender via CLI/env vars does not match the sender configured via
the hardware wallet's HD Path.
Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
            );
    }
    Ok(())
}

/// Initial state.
#[derive(Debug)]
pub struct InitState;

/// State with known [TxKind].
#[derive(Debug)]
pub struct ToState {
    to: Option<Address>,
}

/// State with known input for the transaction.
#[derive(Debug)]
pub struct InputState {
    kind: TxKind,
    input: Vec<u8>,
    func: Option<Function>,
}

pub struct CastTxSender<N, P> {
    provider: P,
    _phantom: PhantomData<N>,
}

impl<N: Network, P: Provider<N>> CastTxSender<N, P>
where
    N::TransactionRequest: FoundryTransactionBuilder<N>,
    N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
{
    /// Creates a new Cast instance responsible for sending transactions.
    pub const fn new(provider: P) -> Self {
        Self { provider, _phantom: PhantomData }
    }

    /// Sends a transaction and waits for receipt synchronously
    pub async fn send_sync(&self, tx: N::TransactionRequest) -> Result<(B256, String)> {
        let mut receipt = TransactionReceiptWithRevertReason::<N> {
            receipt: self.provider.send_transaction_sync(tx).await?,
            revert_reason: None,
        };
        let tx_hash = receipt.receipt.transaction_hash();
        // Allow to fail silently
        let _ = receipt.update_revert_reason(&self.provider).await;

        self.format_receipt(receipt, None).map(|formatted| (tx_hash, formatted))
    }

    /// Sends a transaction to the specified address
    ///
    /// # Example
    ///
    /// ```
    /// use cast::tx::CastTxSender;
    /// use alloy_primitives::{Address, U256, Bytes};
    /// use alloy_serde::WithOtherFields;
    /// use alloy_rpc_types::{TransactionRequest};
    /// use alloy_provider::{RootProvider, ProviderBuilder, network::AnyNetwork};
    /// use std::str::FromStr;
    /// use alloy_sol_types::{sol, SolCall};    ///
    ///
    /// sol!(
    ///     function greet(string greeting) public;
    /// );
    ///
    /// # async fn foo() -> eyre::Result<()> {
    /// let provider = ProviderBuilder::<_,_, AnyNetwork>::default().connect("http://localhost:8545").await?;;
    /// let from = Address::from_str("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")?;
    /// let to = Address::from_str("0xB3C95ff08316fb2F2e3E52Ee82F8e7b605Aa1304")?;
    /// let greeting = greetCall { greeting: "hello".to_string() }.abi_encode();
    /// let bytes = Bytes::from_iter(greeting.iter());
    /// let gas = U256::from_str("200000").unwrap();
    /// let value = U256::from_str("1").unwrap();
    /// let nonce = U256::from_str("1").unwrap();
    /// let tx = TransactionRequest::default().to(to).input(bytes.into()).from(from);
    /// let tx = WithOtherFields::new(tx);
    /// let cast = CastTxSender::new(provider);
    /// let data = cast.send(tx).await?;
    /// println!("{:#?}", data);
    /// # Ok(())
    /// # }
    /// ```
    pub async fn send(&self, tx: N::TransactionRequest) -> Result<PendingTransactionBuilder<N>> {
        let res = self.provider.send_transaction(tx).await?;

        Ok(res)
    }

    /// Sends a raw RLP-encoded transaction via `eth_sendRawTransaction`.
    ///
    /// Used for transaction types that the standard Alloy network stack doesn't understand
    /// (e.g., Tempo transactions).
    pub async fn send_raw(&self, raw_tx: &[u8]) -> Result<PendingTransactionBuilder<N>> {
        let res = self.provider.send_raw_transaction(raw_tx).await?;
        Ok(res)
    }

    /// Sends a raw RLP-encoded transaction and waits for its receipt synchronously.
    pub async fn send_raw_sync(&self, raw_tx: &[u8]) -> Result<(B256, String)> {
        let mut receipt = TransactionReceiptWithRevertReason::<N> {
            receipt: self.provider.send_raw_transaction_sync(raw_tx).await?,
            revert_reason: None,
        };
        let tx_hash = receipt.receipt.transaction_hash();
        // Allow this to fail silently.
        let _ = receipt.update_revert_reason(&self.provider).await;

        self.format_receipt(receipt, None).map(|formatted| (tx_hash, formatted))
    }

    /// Prints the transaction hash (if async) or waits for the receipt and prints it.
    ///
    /// This is the shared "output" path used by both the normal send flow and the browser wallet
    /// flow (which sends the transaction out-of-band and only has a tx hash).
    pub async fn print_tx_result(
        &self,
        tx_hash: B256,
        cast_async: bool,
        confs: u64,
        timeout: u64,
    ) -> Result<()> {
        if cast_async {
            sh_println!("{tx_hash:#x}")?;
        } else {
            let receipt =
                self.receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false).await?;
            sh_println!("{receipt}")?;
        }
        Ok(())
    }

    /// # Example
    ///
    /// ```
    /// use alloy_provider::{ProviderBuilder, RootProvider, network::AnyNetwork};
    /// use cast::tx::CastTxSender;
    ///
    /// async fn foo() -> eyre::Result<()> {
    /// let provider =
    ///     ProviderBuilder::<_, _, AnyNetwork>::default().connect("http://localhost:8545").await?;
    /// let cast = CastTxSender::new(provider);
    /// let tx_hash = "0xf8d1713ea15a81482958fb7ddf884baee8d3bcc478c5f2f604e008dc788ee4fc";
    /// let receipt = cast.receipt(tx_hash.to_string(), None, 1, None, false).await?;
    /// println!("{}", receipt);
    /// # Ok(())
    /// # }
    /// ```
    pub async fn receipt(
        &self,
        tx_hash: String,
        field: Option<String>,
        confs: u64,
        timeout: Option<u64>,
        cast_async: bool,
    ) -> Result<String> {
        let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?;

        let mut receipt = TransactionReceiptWithRevertReason::<N> {
            receipt: match self.provider.get_transaction_receipt(tx_hash).await? {
                Some(r) => r,
                None => {
                    // if the async flag is provided, immediately exit if no tx is found, otherwise
                    // try to poll for it
                    if cast_async {
                        eyre::bail!("tx not found: {:?}", tx_hash);
                    }
                    PendingTransactionBuilder::<N>::new(self.provider.root().clone(), tx_hash)
                        .with_required_confirmations(confs)
                        .with_timeout(timeout.map(Duration::from_secs))
                        .get_receipt()
                        .await?
                }
            },
            revert_reason: None,
        };

        // Allow to fail silently
        let _ = receipt.update_revert_reason(&self.provider).await;

        self.format_receipt(receipt, field)
    }

    /// Helper method to format transaction receipts consistently
    fn format_receipt(
        &self,
        receipt: TransactionReceiptWithRevertReason<N>,
        field: Option<String>,
    ) -> Result<String> {
        Ok(if let Some(ref field) = field {
            get_pretty_receipt_w_reason_attr(&receipt, field)
                .ok_or_else(|| eyre::eyre!("invalid receipt field: {}", field))?
        } else if shell::is_json() {
            // to_value first to sort json object keys
            serde_json::to_value(&receipt)?.to_string()
        } else {
            receipt.pretty()
        })
    }
}

/// Builder type constructing generic TransactionRequest from cast send/mktx inputs.
///
/// It is implemented as a stateful builder with expected state transition of [InitState] ->
/// [ToState] -> [InputState].
#[derive(Debug)]
pub struct CastTxBuilder<N: Network, P, S> {
    provider: P,
    pub(crate) tx: N::TransactionRequest,
    /// Whether the transaction should be sent as a legacy transaction.
    legacy: bool,
    blob: bool,
    /// Whether the blob transaction should use EIP-4844 (legacy) format instead of EIP-7594.
    eip4844: bool,
    /// Whether to fill gas, fees and nonce. Set to `false` for read-only calls
    /// (eth_call, eth_estimateGas, eth_createAccessList).
    fill: bool,
    /// Whether the filled transaction will be submitted through a browser wallet.
    browser: bool,
    /// The preset used when estimating EIP-1559 fees.
    eip1559_fee_estimate: Eip1559FeeEstimatePreset,
    auth: Vec<CliAuthorizationList>,
    chain: Chain,
    etherscan_api_key: Option<String>,
    etherscan_api_url: Option<String>,
    access_list: Option<Option<AccessList>>,
    state: S,
}

impl<N: Network, P, S> CastTxBuilder<N, P, S> {
    /// Returns the resolved chain for this builder.
    pub const fn chain(&self) -> Chain {
        self.chain
    }

    /// Marks this transaction as destined for browser wallet submission.
    pub const fn with_browser_wallet(mut self) -> Self {
        self.browser = true;
        self
    }
}

impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InitState>
where
    N::TransactionRequest: FoundryTransactionBuilder<N>,
{
    /// Creates a new instance of [CastTxBuilder] filling transaction with fields present in
    /// provided [TransactionOpts].
    pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
        let mut tx = N::TransactionRequest::default();

        let chain = utils::get_chain(config.chain, &provider).await?;
        let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten();
        let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone());
        let etherscan_api_url = etherscan_config.map(|c| c.api_url);
        // mark it as legacy if requested or the chain is legacy and no 7702 is provided.
        let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());

        // Apply gas, value, fee, and network-specific options.
        tx_opts.apply::<N>(&mut tx, legacy);

        Ok(Self {
            provider,
            tx,
            legacy,
            blob: tx_opts.blob,
            eip4844: tx_opts.eip4844,
            fill: true,
            browser: false,
            eip1559_fee_estimate: config.eip1559_fee_estimate,
            chain,
            etherscan_api_key,
            etherscan_api_url,
            auth: tx_opts.auth,
            access_list: tx_opts.access_list,
            state: InitState,
        })
    }

    /// Sets [TxKind] for this builder and changes state to [ToState].
    pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<N, P, ToState>> {
        let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
        Ok(CastTxBuilder {
            provider: self.provider,
            tx: self.tx,
            legacy: self.legacy,
            blob: self.blob,
            eip4844: self.eip4844,
            fill: self.fill,
            browser: self.browser,
            eip1559_fee_estimate: self.eip1559_fee_estimate,
            chain: self.chain,
            etherscan_api_key: self.etherscan_api_key,
            etherscan_api_url: self.etherscan_api_url,
            auth: self.auth,
            access_list: self.access_list,
            state: ToState { to },
        })
    }
}

impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, ToState>
where
    N::TransactionRequest: FoundryTransactionBuilder<N>,
{
    /// Accepts user-provided code, sig and args params and constructs calldata for the transaction.
    /// If code is present, input will be set to code + encoded constructor arguments. If no code is
    /// present, input is set to just provided arguments.
    pub async fn with_code_sig_and_args(
        self,
        code: Option<String>,
        sig: Option<String>,
        args: Vec<String>,
    ) -> Result<CastTxBuilder<N, P, InputState>> {
        let (mut args, func) = if let Some(sig) = sig {
            parse_function_args(
                &sig,
                args,
                self.state.to,
                self.chain,
                &self.provider,
                self.etherscan_api_key.as_deref(),
                self.etherscan_api_url.as_deref(),
            )
            .await?
        } else {
            (Vec::new(), None)
        };

        let input = if let Some(code) = &code {
            let mut code = hex::decode(code)?;
            code.append(&mut args);
            code
        } else {
            args
        };

        if self.state.to.is_none() && code.is_none() {
            let has_value = self.tx.value().is_some_and(|v| !v.is_zero());
            let has_auth = !self.auth.is_empty();
            // We only allow user to omit the recipient address if transaction is an EIP-7702 tx
            // without a value.
            if !has_auth || has_value {
                eyre::bail!("Must specify a recipient address or contract code to deploy");
            }
        }

        Ok(CastTxBuilder {
            provider: self.provider,
            tx: self.tx,
            legacy: self.legacy,
            blob: self.blob,
            eip4844: self.eip4844,
            fill: self.fill,
            browser: self.browser,
            eip1559_fee_estimate: self.eip1559_fee_estimate,
            chain: self.chain,
            etherscan_api_key: self.etherscan_api_key,
            etherscan_api_url: self.etherscan_api_url,
            auth: self.auth,
            access_list: self.access_list,
            state: InputState { kind: self.state.to.into(), input, func },
        })
    }
}

impl<N: Network, P: Provider<N>> CastTxBuilder<N, P, InputState>
where
    N::TransactionRequest: FoundryTransactionBuilder<N>,
{
    /// Builds the TransactionRequest. Fills gas, fees and nonce unless [`raw`](Self::raw) was
    /// called.
    pub async fn build(
        self,
        sender: impl Into<SenderKind<'_>>,
    ) -> Result<(N::TransactionRequest, Option<Function>)> {
        let fill = self.fill;
        self._build(sender, fill, None).await
    }

    /// Builds a transaction that will be signed by a Tempo access key.
    ///
    /// The access-key id is set before gas estimation. If the access key needs on-chain
    /// provisioning, its authorization is embedded before access-list/gas estimation and before
    /// any sponsor digest can be computed.
    pub async fn build_with_tempo_wallet(
        self,
        wallet: &TempoAccountsWallet,
    ) -> Result<(N::TransactionRequest, Option<Function>, TempoAccountsWallet)> {
        let fill = self.fill;
        let mut prepared = wallet.clone();
        let (tx, func) = self._build(wallet.account(), fill, Some(&mut prepared)).await?;
        Ok((tx, func, prepared))
    }

    async fn _build(
        mut self,
        sender: impl Into<SenderKind<'_>>,
        fill: bool,
        tempo_wallet: Option<&mut TempoAccountsWallet>,
    ) -> Result<(N::TransactionRequest, Option<Function>)> {
        // prepare
        let sender = sender.into();
        self.prepare(&sender);

        // For batch transactions with calls, clear `to` and `value` so the node correctly
        // identifies this as an AA batch transaction. The `calls` field determines the actual
        // targets. If `to` is set, `build_aa()` would add a spurious extra call.
        self.tx.clear_batch_to();

        // resolve
        // Read-only calls do not need a nonce unless it is required to sign an authorization.
        // Avoid an otherwise unused `eth_getTransactionCount` request for raw transactions.
        let resolve_in_parallel =
            fill && self.auth.is_empty() && tempo_wallet.is_none() && !self.chain.is_tempo();
        let tx_nonce = if resolve_in_parallel {
            let nonce = self.tx.nonce();
            let fees_are_complete = if self.legacy {
                self.tx.gas_price().is_some()
            } else {
                matches!(
                    (self.tx.max_fee_per_gas(), self.tx.max_priority_fee_per_gas()),
                    (Some(max_fee), Some(priority_fee)) if priority_fee <= max_fee
                )
            } && (!self.blob || self.tx.max_fee_per_blob_gas().is_some());
            let gas_request =
                (fees_are_complete && self.access_list.is_none() && self.tx.gas_limit().is_none())
                    .then(|| self.tx.clone());
            let (tx_nonce, (), gas_limit) = tokio::try_join!(
                Self::resolve_nonce(&self.provider, sender.address(), nonce),
                Self::fill_fees(
                    &self.provider,
                    &mut self.tx,
                    self.blob,
                    self.legacy,
                    self.browser,
                    self.eip1559_fee_estimate,
                ),
                async {
                    match gas_request {
                        Some(request) => {
                            Self::estimate_gas(&self.provider, request).await.map(Some)
                        }
                        None => Ok(None),
                    }
                },
            )?;
            if let Some(gas_limit) = gas_limit {
                self.tx.set_gas_limit(gas_limit);
            }
            Some(tx_nonce)
        } else if fill || !self.auth.is_empty() {
            Some(Self::resolve_nonce(&self.provider, sender.address(), self.tx.nonce()).await?)
        } else {
            None
        };
        if let Some(tx_nonce) = tx_nonce {
            if fill {
                self.tx.set_nonce(tx_nonce);
            }
            self.resolve_auth(&sender, tx_nonce).await?;
        }
        if let Some(wallet) = tempo_wallet {
            *wallet = self.tx.prepare_with_tempo_wallet(&self.provider, wallet).await?;
        }
        if fill && !resolve_in_parallel {
            Self::fill_fees(
                &self.provider,
                &mut self.tx,
                self.blob,
                self.legacy,
                self.browser,
                self.eip1559_fee_estimate,
            )
            .await?;
        }
        self.resolve_access_list().await?;
        if fill {
            self.fill_gas_limit().await?;
        }

        Ok((self.tx, self.state.func))
    }

    /// Sets the core transaction fields from the builder state: kind, input, optional from, and
    /// chain id.
    fn prepare(&mut self, sender: &SenderKind<'_>) {
        self.tx.set_kind(self.state.kind);
        // We set both fields to the same value because some nodes only accept the legacy
        // `data` field: https://github.com/foundry-rs/foundry/issues/7764#issuecomment-2210453249
        self.tx.set_input_kind(self.state.input.clone(), TransactionInputKind::Both);
        let sender = sender.address();
        if !sender.is_zero() {
            self.tx.set_from(sender);
        }
        self.tx.set_chain_id(self.chain.id());
    }

    /// Resolves the transaction nonce. Returns the existing nonce or fetches one from the provider.
    async fn resolve_nonce(provider: &P, from: Address, nonce: Option<u64>) -> Result<u64> {
        if let Some(nonce) = nonce {
            Ok(nonce)
        } else {
            Ok(provider.get_transaction_count(from).await?)
        }
    }

    /// Resolves the access list. Fetches from the provider if `--access-list` was passed without
    /// a value.
    async fn resolve_access_list(&mut self) -> Result<()> {
        if let Some(access_list) = match self.access_list.take() {
            None => None,
            Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
            Some(Some(access_list)) => Some(access_list),
        } {
            self.tx.set_access_list(access_list);
        }
        Ok(())
    }

    /// Parses the passed --auth values and sets the authorization list on the transaction.
    ///
    /// If a signer is available in `sender`, address-based auths will be signed.
    /// If no signer is available, all auths must be pre-signed.
    async fn resolve_auth(&mut self, sender: &SenderKind<'_>, tx_nonce: u64) -> Result<()> {
        if self.auth.is_empty() {
            return Ok(());
        }

        let auths = std::mem::take(&mut self.auth);

        // Validate that at most one address-based auth is provided (multiple addresses are
        // almost always unintended).
        let address_auth_count =
            auths.iter().filter(|a| matches!(a, CliAuthorizationList::Address(_))).count();
        if address_auth_count > 1 {
            eyre::bail!(
                "Multiple address-based authorizations provided. Only one address can be specified; \
                use pre-signed authorizations (hex-encoded) for multiple authorizations."
            );
        }

        let mut signed_auths = Vec::with_capacity(auths.len());

        for auth in auths {
            let signed_auth = match auth {
                CliAuthorizationList::Address(address) => {
                    let auth = Authorization {
                        chain_id: U256::from(self.chain.id()),
                        nonce: tx_nonce + 1,
                        address,
                    };

                    let Some(signer) = sender.as_signer() else {
                        eyre::bail!(
                            "No signer available to sign authorization. \
                            Provide a pre-signed authorization (hex-encoded) instead."
                        );
                    };
                    let signature = signer.sign_hash(&auth.signature_hash()).await?;

                    auth.into_signed(signature)
                }
                CliAuthorizationList::Signed(auth) => auth,
            };
            signed_auths.push(signed_auth);
        }

        self.tx.set_authorization_list(signed_auths);

        Ok(())
    }

    /// Fills gas price, EIP-1559 fees, and blob fees from the provider.
    ///
    /// Only fills values that haven't been explicitly set by the user.
    async fn fill_fees(
        provider: &P,
        tx: &mut N::TransactionRequest,
        blob: bool,
        legacy: bool,
        browser: bool,
        eip1559_fee_estimate: Eip1559FeeEstimatePreset,
    ) -> Result<()> {
        if blob && tx.max_fee_per_blob_gas().is_none() {
            tx.set_max_fee_per_blob_gas(provider.get_blob_base_fee().await?)
        }

        fill_transaction_gas_fees(provider, tx, legacy, browser, eip1559_fee_estimate).await
    }

    /// Fills gas limit from the provider.
    async fn fill_gas_limit(&mut self) -> Result<()> {
        if self.tx.gas_limit().is_none() {
            let request = if self.browser && self.chain.is_tempo() {
                self.tx.browser_wallet_gas_estimation_request()
            } else {
                self.tx.clone()
            };
            let estimated = Self::estimate_gas(&self.provider, request).await?;
            self.tx.set_gas_limit(estimated);
        }

        Ok(())
    }

    /// Estimate tx gas from provider call. Tries to decode custom error if execution reverted.
    async fn estimate_gas(provider: &P, request: N::TransactionRequest) -> Result<u64> {
        match provider.estimate_gas(request).await {
            Ok(estimated) => Ok(estimated),
            Err(err) => {
                if let TransportError::ErrorResp(payload) = &err {
                    // If execution reverted with code 3 during provider gas estimation then try
                    // to decode custom errors and append it to the error message.
                    if payload.code == 3
                        && let Some(data) = &payload.data
                        && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
                    {
                        eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error);
                    }
                }
                eyre::bail!("Failed to estimate gas: {}", err);
            }
        }
    }

    /// Populates the blob sidecar for the transaction if any blob data was provided.
    pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
        let Some(blob_data) = blob_data else { return Ok(self) };

        let mut coder = SidecarBuilder::<SimpleCoder>::default();
        coder.ingest(&blob_data);

        if self.eip4844 {
            let sidecar = coder.build_4844()?;
            self.tx.set_blob_sidecar_4844(sidecar);
        } else {
            let sidecar = coder.build_7594()?;
            self.tx.set_blob_sidecar_7594(sidecar);
        }

        Ok(self)
    }

    /// Skips gas, fee and nonce filling. Use for read-only calls
    /// (eth_call, eth_estimateGas, eth_createAccessList).
    pub const fn raw(mut self) -> Self {
        self.fill = false;
        self
    }
}

/// Fills gas price or EIP-1559 fee fields from the provider and validates the final pair.
pub(crate) async fn fill_transaction_gas_fees<N: Network, P: Provider<N>>(
    provider: &P,
    tx: &mut N::TransactionRequest,
    legacy: bool,
    browser: bool,
    eip1559_fee_estimate: Eip1559FeeEstimatePreset,
) -> Result<()>
where
    N::TransactionRequest: FoundryTransactionBuilder<N>,
{
    if legacy {
        if tx.gas_price().is_none() {
            tx.set_gas_price(provider.get_gas_price().await?);
        }
        return Ok(());
    }

    if tx.max_fee_per_gas().is_none() || tx.max_priority_fee_per_gas().is_none() {
        let estimate = estimate_eip1559_fees(provider, eip1559_fee_estimate).await?;

        // Only honor the browser-suggested tip when the user has not pinned a
        // priority fee; `resolve_broadcast_eip1559_fees` ignores a lower tip.
        let browser_suggested_tip = if browser && tx.max_priority_fee_per_gas().is_none() {
            provider.get_max_priority_fee_per_gas().await.ok()
        } else {
            None
        };

        // User `--gas-price`/`--priority-gas-price` overrides are already applied
        // to `tx`; pass `None` so they are not double-applied here.
        let estimate = resolve_broadcast_eip1559_fees(estimate, None, None, browser_suggested_tip)?;

        if tx.max_fee_per_gas().is_none() {
            tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
        }

        if tx.max_priority_fee_per_gas().is_none() {
            tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
        }
    }

    if let (Some(max_fee), Some(priority)) = (tx.max_fee_per_gas(), tx.max_priority_fee_per_gas()) {
        eyre::ensure!(
            priority <= max_fee,
            "max priority fee per gas ({priority}) cannot exceed max fee per gas ({max_fee})"
        );
    }

    Ok(())
}

/// Helper function that tries to decode custom error name and inputs from error payload data.
async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
    let err_data = serde_json::from_str::<Bytes>(data.get())?;
    let Some(selector) = err_data.get(..4) else { return Ok(None) };
    if let Some(known_error) =
        SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
    {
        let mut decoded_error = known_error.name.clone();
        if !known_error.inputs.is_empty()
            && let Ok(error) = known_error.decode_error(&err_data)
        {
            write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
        }
        return Ok(Some(decoded_error));
    }
    Ok(None)
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloy_json_rpc::{RequestPacket, ResponsePacket};
    use alloy_network::Ethereum;
    use alloy_provider::{ProviderBuilder, mock::Asserter};
    use alloy_rpc_client::RpcClient;
    use alloy_transport::{TransportFut, mock::MockTransport};
    use clap::Parser;
    use std::{
        sync::{Arc, Mutex},
        task::{Context, Poll},
    };
    use tokio::{sync::Barrier, time::timeout};
    use tower::Service;

    #[derive(Clone)]
    struct BarrierTransport {
        inner: MockTransport,
        barrier: Arc<Barrier>,
        fill_methods: Arc<Mutex<Vec<String>>>,
    }

    impl Service<RequestPacket> for BarrierTransport {
        type Response = ResponsePacket;
        type Error = TransportError;
        type Future = TransportFut<'static>;

        fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
            self.inner.poll_ready(cx)
        }

        fn call(&mut self, req: RequestPacket) -> Self::Future {
            let fill_method = match &req {
                RequestPacket::Single(req)
                    if matches!(
                        req.method(),
                        "eth_getTransactionCount" | "eth_gasPrice" | "eth_estimateGas"
                    ) =>
                {
                    Some(req.method().to_string())
                }
                _ => None,
            };
            let Some(fill_method) = fill_method else {
                return self.inner.call(req);
            };
            self.fill_methods.lock().unwrap().push(fill_method);

            let barrier = self.barrier.clone();
            let mut inner = self.inner.clone();
            Box::pin(async move {
                barrier.wait().await;
                inner.call(req).await
            })
        }
    }

    #[tokio::test]
    async fn filled_build_fetches_nonce_and_gas_concurrently_with_explicit_fees() {
        let asserter = Asserter::new();
        for _ in 0..2 {
            asserter.push_success(&U64::from(1));
        }
        let fill_methods = Arc::new(Mutex::new(Vec::new()));
        let transport = BarrierTransport {
            inner: MockTransport::new(asserter),
            barrier: Arc::new(Barrier::new(2)),
            fill_methods: fill_methods.clone(),
        };
        let provider = ProviderBuilder::new_with_network::<Ethereum>()
            .connect_client(RpcClient::new(transport, true));
        let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };

        let builder = CastTxBuilder::new(
            &provider,
            TransactionOpts::parse_from(["test", "--legacy", "--gas-price", "1"]),
            &config,
        )
        .await
        .unwrap()
        .with_to(Some(Address::repeat_byte(0x11).into()))
        .await
        .unwrap()
        .with_code_sig_and_args(None, None, Vec::new())
        .await
        .unwrap();
        let (tx, _) = timeout(Duration::from_secs(1), builder.build(Address::repeat_byte(0x22)))
            .await
            .expect("nonce and gas requests were not in flight together")
            .unwrap();

        assert_eq!(tx.nonce, Some(1));
        assert_eq!(tx.gas_price, Some(1));
        assert_eq!(tx.gas, Some(1));
        let mut fill_methods = fill_methods.lock().unwrap().clone();
        fill_methods.sort();
        assert_eq!(fill_methods, ["eth_estimateGas", "eth_getTransactionCount"]);
    }

    #[tokio::test]
    async fn raw_build_skips_nonce_request() {
        // No responses are queued, so any RPC request would fail this test. In particular, this
        // guards against restoring the unused `eth_getTransactionCount` request.
        let provider =
            ProviderBuilder::new_with_network::<Ethereum>().connect_mocked_client(Asserter::new());
        let config = Config { chain: Some(Chain::mainnet()), ..Default::default() };

        CastTxBuilder::new(&provider, TransactionOpts::parse_from(["test"]), &config)
            .await
            .unwrap()
            .with_to(Some(Address::repeat_byte(0x11).into()))
            .await
            .unwrap()
            .with_code_sig_and_args(None, None, Vec::new())
            .await
            .unwrap()
            .raw()
            .build(Address::repeat_byte(0x22))
            .await
            .unwrap();
    }
}
