use crate::cmd::install;
use alloy_chains::Chain;
use alloy_consensus::{SignableTransaction, Signed};
use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
use alloy_json_abi::JsonAbi;
use alloy_network::{Ethereum, EthereumWallet, Network, ReceiptResponse, TransactionBuilder};
use alloy_primitives::{Address, Bytes, U256, hex};
use alloy_provider::{PendingTransactionError, Provider, ProviderBuilder as AlloyProviderBuilder};
use alloy_signer::{Signature, Signer};
use alloy_transport::TransportError;
use clap::{Parser, ValueHint};
use eyre::{Context, ContextCompat, Result};
use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs, parse_etherscan_license_type};
use foundry_cli::{
    opts::{BuildOpts, EthereumOpts, EtherscanOpts, TransactionOpts},
    utils::{
        LoadConfig, ResolvedLane, find_contract_artifacts, maybe_print_resolved_lane,
        parse_constructor_args, read_constructor_args_file, resolve_lane,
    },
};
use foundry_common::{
    FoundryTransactionBuilder,
    compile::{self},
    provider::{
        ProviderBuilder,
        fee::{estimate_eip1559_fees, resolve_broadcast_eip1559_fees},
    },
    shell,
    tempo::{maybe_print_fee_token, resolve_and_set_fee_token},
};
use foundry_compilers::{
    ArtifactId, artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize,
};
use foundry_config::{
    Config, Eip1559FeeEstimatePreset,
    figment::{
        self, Metadata, Profile,
        value::{Dict, Map},
    },
    merge_impl_figment_convert,
};
use foundry_wallets::{
    BrowserWalletOpts, TempoAccountsWallet, WalletSigner, wallet_browser::signer::BrowserSigner,
};
use serde_json::json;
use std::{borrow::Borrow, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration};
use tempo_alloy::TempoNetwork;

merge_impl_figment_convert!(CreateArgs, build, eth);

/// CLI arguments for `forge create`.
#[derive(Clone, Debug, Parser)]
pub struct CreateArgs {
    /// The contract identifier in the form `<path>:<contractname>`.
    contract: ContractInfo,

    /// The constructor arguments.
    #[arg(
        long,
        num_args(1..),
        conflicts_with = "constructor_args_path",
        value_name = "ARGS",
        allow_hyphen_values = true,
    )]
    constructor_args: Vec<String>,

    /// The path to a file containing the constructor arguments.
    #[arg(
        long,
        value_hint = ValueHint::FilePath,
        value_name = "PATH",
    )]
    constructor_args_path: Option<PathBuf>,

    /// Broadcast the transaction.
    #[arg(long)]
    pub broadcast: bool,

    /// Verify contract after creation.
    #[arg(long)]
    verify: bool,

    /// Send via `eth_sendTransaction` using the `--from` argument or `$ETH_FROM` as sender
    #[arg(long, requires = "from")]
    unlocked: bool,

    /// Prints the standard json compiler input if `--verify` is provided.
    ///
    /// The standard json compiler input can be used to manually submit contract verification in
    /// the browser.
    #[arg(long, requires = "verify")]
    show_standard_json_input: bool,

    /// The Etherscan license type code or SPDX identifier to include with the verification
    /// request.
    ///
    /// Accepts either an Etherscan numeric license code or a common SPDX identifier such as `MIT`.
    /// This is only used for Etherscan-style verifiers when `--verify` is enabled.
    #[arg(
        long,
        requires = "verify",
        value_name = "LICENSE",
        help_heading = "Verifier options",
        value_parser = parse_etherscan_license_type,
    )]
    license_type: Option<String>,

    /// Timeout to use for broadcasting transactions.
    #[arg(long, env = "ETH_TIMEOUT")]
    pub timeout: Option<u64>,

    #[command(flatten)]
    build: BuildOpts,

    #[command(flatten)]
    tx: TransactionOpts,

    #[command(flatten)]
    eth: EthereumOpts,

    #[command(flatten)]
    pub verifier: VerifierArgs,

    #[command(flatten)]
    retry: RetryArgs,

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

impl CreateArgs {
    /// Executes the command to create a contract
    pub async fn run(mut self) -> Result<()> {
        if self.tx.tempo.sponsor_url.is_some() {
            eyre::bail!(
                "--sponsor-url is not supported by forge create; use --tempo.sponsor with \
                 --tempo.sponsor-signer or --tempo.sponsor-sig"
            );
        }

        // Resolve chain early so we can dispatch to the correct network type.
        let chain = if let Some(chain) = self.chain_id() {
            chain
        } else {
            let config = self.load_config()?;
            let provider = ProviderBuilder::<Ethereum>::from_config(&config)?.build()?;
            let chain_id = provider.get_chain_id().await?;
            let chain = Chain::from(chain_id);
            self.eth.etherscan.chain = Some(chain);
            chain
        };
        let mut wallet = self.eth.wallet.clone();
        if !chain.is_tempo() && !self.tx.tempo.is_tempo() {
            // Do not let a matching entry in the Tempo Accounts store change an ordinary Ethereum
            // deployment into a Tempo transaction.
            wallet.from = None;
        }
        let (signer, tempo_access_key) = wallet.maybe_signer_for_chain(chain.id()).await?;

        if tempo_access_key.is_some() || self.tx.tempo.is_tempo() || chain.is_tempo() {
            self.run_generic::<TempoNetwork>(signer, tempo_access_key).await
        } else {
            self.run_generic::<Ethereum>(signer, None).await
        }
    }

    async fn run_generic<N: Network>(
        mut self,
        pre_resolved_signer: Option<WalletSigner>,
        access_key: Option<TempoAccountsWallet>,
    ) -> Result<()>
    where
        N::TxEnvelope: From<Signed<N::UnsignedTx>>,
        N::UnsignedTx: SignableTransaction<Signature>,
        N::TransactionRequest: FoundryTransactionBuilder<N> + serde::Serialize,
        N::ReceiptResponse: serde::Serialize,
    {
        let mut config = self.load_config()?;
        let resolve_unknown_fee_token_symbol = !config.eth_rpc_curl;

        // Install missing dependencies.
        if install::install_missing_dependencies(&mut config).await && config.auto_detect_remappings
        {
            // need to re-configure here to also catch additional remappings
            config = self.load_config()?;
        }

        // Find Project & Compile
        let project = config.project()?;

        let target_path = if let Some(ref mut path) = self.contract.path {
            canonicalize(project.root().join(path))?
        } else {
            project.find_contract_path(&self.contract.name)?
        };

        let output = compile::compile_target(&target_path, &project, shell::is_json())?;

        let (abi, bin, id) = find_contract_artifacts(output, &target_path, &self.contract.name)?;

        let bin = match bin.object {
            BytecodeObject::Bytecode(_) => bin.object,
            _ => {
                let link_refs = bin
                    .link_references
                    .iter()
                    .flat_map(|(path, names)| {
                        names.keys().map(move |name| format!("\t{name}: {path}"))
                    })
                    .collect::<Vec<String>>()
                    .join("\n");
                eyre::bail!(
                    "Dynamic linking not supported in `create` command - deploy the following library contracts first, then provide the address to link at compile time\n{}",
                    link_refs
                );
            }
        };

        // Add arguments to constructor
        let params = if let Some(constructor) = &abi.constructor {
            let constructor_args =
                self.constructor_args_path.clone().map(read_constructor_args_file).transpose()?;
            parse_constructor_args(
                constructor,
                constructor_args.as_deref().unwrap_or(&self.constructor_args),
            )?
        } else {
            if !self.constructor_args.is_empty() || self.constructor_args_path.is_some() {
                sh_warn!(
                    "`{}` has no constructor; ignoring provided constructor arguments",
                    self.contract.name
                )?;
            }
            vec![]
        };

        let provider = ProviderBuilder::<N>::from_config(&config)?.build()?;

        // Inject access key ID into TempoOpts so it's set before gas estimation.
        if let Some(ref ak) = access_key {
            self.tx.tempo.key_id = Some(ak.key_id()?);
        }

        // Resolve `--tempo.lane <name>` against the lanes file (default
        // `<root>/tempo.lanes.toml`) and populate `self.tx.tempo.nonce_key` from the lane.
        // Must happen before `self.deploy(...)` so `TempoOpts::apply` picks up the nonce_key.
        let resolved_lane = resolve_lane(&mut self.tx.tempo, &config.root)?;
        let expires_at = self.tx.tempo.resolve_expires();

        // Whether to broadcast the transaction or not
        let dry_run = !self.broadcast;

        // Launch browser signer if `--browser` flag is set
        let browser = self.browser.run::<N>().await?;

        if let Some(browser) = browser {
            // Deploy with browser wallet
            let deployer_address = browser.address();
            self.deploy(
                abi,
                bin,
                params,
                provider,
                deployer_address,
                config.transaction_timeout,
                id,
                dry_run,
                None,
                Some(browser),
                resolved_lane,
                expires_at,
                resolve_unknown_fee_token_symbol,
                config.eip1559_fee_estimate,
            )
            .await
        } else if self.unlocked {
            // Deploy with unlocked account
            let sender = self.eth.wallet.from.expect("required");
            self.deploy(
                abi,
                bin,
                params,
                provider,
                sender,
                config.transaction_timeout,
                id,
                dry_run,
                None,
                None,
                resolved_lane,
                expires_at,
                resolve_unknown_fee_token_symbol,
                config.eip1559_fee_estimate,
            )
            .await
        } else if let Some(ak) = access_key {
            let deployer_address = ak.account();
            self.deploy(
                abi,
                bin,
                params,
                provider,
                deployer_address,
                config.transaction_timeout,
                id,
                dry_run,
                Some(ak),
                None,
                resolved_lane,
                expires_at,
                resolve_unknown_fee_token_symbol,
                config.eip1559_fee_estimate,
            )
            .await
        } else {
            // Deploy with signer
            let signer = match pre_resolved_signer {
                Some(s) => s,
                None => self.eth.wallet.signer().await?,
            };
            let deployer = signer.address();
            let provider = AlloyProviderBuilder::<_, _, N>::default()
                .wallet(EthereumWallet::new(signer))
                .connect_provider(provider);
            self.deploy(
                abi,
                bin,
                params,
                provider,
                deployer,
                config.transaction_timeout,
                id,
                dry_run,
                None,
                None,
                resolved_lane,
                expires_at,
                resolve_unknown_fee_token_symbol,
                config.eip1559_fee_estimate,
            )
            .await
        }
    }

    /// Returns the resolved chain, if any.
    const fn chain_id(&self) -> Option<Chain> {
        self.eth.etherscan.chain
    }

    /// Ensures the verify command can be executed.
    ///
    /// This is supposed to check any things that might go wrong when preparing a verify request
    /// before the contract is deployed. This should prevent situations where a contract is deployed
    /// successfully, but we fail to prepare a verify request which would require manual
    /// verification.
    async fn verify_preflight_check(
        &self,
        constructor_args: Option<String>,
        id: &ArtifactId,
    ) -> Result<()> {
        // NOTE: this does not represent the same `VerifyArgs` that would be sent after deployment,
        // since we don't know the address yet.
        let mut verify = VerifyArgs {
            address: Default::default(),
            contract: Some(self.contract.clone()),
            compiler_version: Some(id.version.to_string()),
            constructor_args,
            constructor_args_path: None,
            no_auto_detect: false,
            use_solc: None,
            num_of_optimizations: None,
            etherscan: EtherscanOpts {
                key: self.eth.etherscan.key.clone(),
                chain: self.chain_id(),
            },
            rpc: Default::default(),
            flatten: false,
            force: false,
            skip_is_verified_check: true,
            watch: true,
            print_submission_result_to_stdout: false,
            retry: self.retry,
            libraries: self.build.libraries.clone(),
            root: None,
            verifier: self.verifier.clone(),
            via_ir: self.build.compiler.via_ir,
            license_type: self.license_type.clone(),
            evm_version: self.build.compiler.evm_version,
            show_standard_json_input: self.show_standard_json_input,
            guess_constructor_args: false,
            compilation_profile: Some(id.profile.clone()),
            language: None,
            creation_transaction_hash: None,
        };

        // Check config for Etherscan API Keys to avoid preflight check failing if no
        // ETHERSCAN_API_KEY value set.
        let config = verify.load_config()?;
        verify.etherscan.key = config
            .get_etherscan_config_with_chain(self.chain_id())?
            .map(|c| c.key)
            .or_else(|| config.etherscan_api_key.clone());

        let context = verify.resolve_context().await?;

        verify.verification_provider()?.preflight_verify_check(verify.clone(), context).await?;

        let api_key = verify.verifier.resolve_api_key(verify.etherscan.key.as_deref());
        let chain = verify.etherscan.chain.context("chain ID not resolved")?;
        verify
            .verifier
            .check_credentials(api_key, chain, &config)
            .await
            .wrap_err("Verification preflight check failed")?;

        Ok(())
    }

    /// Deploys the contract
    #[expect(clippy::too_many_arguments)]
    async fn deploy<N: Network, P: Provider<N>>(
        self,
        abi: JsonAbi,
        bin: BytecodeObject,
        args: Vec<DynSolValue>,
        provider: P,
        deployer_address: Address,
        timeout: u64,
        id: ArtifactId,
        dry_run: bool,
        mut tempo_keychain: Option<TempoAccountsWallet>,
        browser_signer: Option<BrowserSigner<N>>,
        resolved_lane: Option<ResolvedLane>,
        expires_at: Option<u64>,
        resolve_unknown_fee_token_symbol: bool,
        eip1559_fee_estimate: Eip1559FeeEstimatePreset,
    ) -> Result<()>
    where
        N::TransactionRequest: FoundryTransactionBuilder<N> + serde::Serialize,
        N::ReceiptResponse: serde::Serialize,
    {
        let chain = self.chain_id().context("chain ID not resolved")?;

        let bin = bin.into_bytes().unwrap_or_default();
        if bin.is_empty() {
            eyre::bail!("no bytecode found in bin object for {}", self.contract.name);
        }

        let provider = Arc::new(provider);
        let factory =
            ContractFactory::<N, _>::new(abi.clone(), bin.clone(), provider.clone(), timeout);

        let is_args_empty = args.is_empty();
        let mut deployer =
            factory.deploy_tokens(args.clone()).context("failed to deploy contract").map_err(|e| {
                if is_args_empty {
                    e.wrap_err("no arguments provided for contract constructor; consider --constructor-args or --constructor-args-path")
                } else {
                    e
                }
            })?;
        let is_legacy = self.tx.legacy || chain.is_legacy();

        deployer.tx.set_from(deployer_address);
        deployer.tx.set_chain_id(chain.id());
        // `to` field must be set explicitly, cannot be None.
        if deployer.tx.to().is_none() {
            deployer.tx.set_create();
        }

        // Apply user-provided gas, fee, nonce, and Tempo options.
        self.tx.apply::<N>(&mut deployer.tx, is_legacy);

        // Convert only AA CREATE transactions into a call entry. Plain Tempo
        // CREATE transactions remain Ethereum transactions, while AA requests
        // (for example, an expiring nonce) require a non-empty `calls` list.
        if deployer.tx.is_tempo_aa() {
            deployer.tx.convert_create_to_call();
        }

        if tempo_keychain.is_some() && deployer.tx.nonce_key().is_none() {
            deployer.tx.set_nonce_key(U256::ZERO);
        }

        // Fetch defaults from provider for values not specified by user.
        if self.tx.nonce.is_none() && !self.tx.tempo.expiring_nonce {
            deployer.tx.set_nonce(provider.get_transaction_count(deployer_address).await?);
        }

        maybe_print_resolved_lane(resolved_lane.as_ref(), deployer.tx.nonce().unwrap_or_default())?;

        if let Some(wallet) = tempo_keychain.as_ref() {
            tempo_keychain =
                Some(deployer.tx.prepare_with_tempo_wallet(provider.as_ref(), wallet).await?);
        }

        if is_legacy {
            if self.tx.gas_price.is_none() {
                deployer.tx.set_gas_price(provider.get_gas_price().await?);
            }
        } else {
            if self.tx.gas_price.is_none() || self.tx.priority_gas_price.is_none() {
                let estimate = estimate_eip1559_fees(&provider, eip1559_fee_estimate).await.wrap_err("Failed to estimate EIP1559 fees. This chain might not support EIP1559, try adding --legacy to your command.")?;

                // 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_signer.is_some() && self.tx.priority_gas_price.is_none() {
                        provider.get_max_priority_fee_per_gas().await.ok()
                    } else {
                        None
                    };

                // User `--gas-price`/`--priority-gas-price` overrides are applied
                // below only for unset fields; pass `None` to avoid double-applying.
                let estimate =
                    resolve_broadcast_eip1559_fees(estimate, None, None, browser_suggested_tip)?;

                if self.tx.priority_gas_price.is_none() {
                    deployer.tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
                }
                if self.tx.gas_price.is_none() {
                    deployer.tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
                }
            }
            if let (Some(max_fee), Some(priority)) =
                (deployer.tx.max_fee_per_gas(), deployer.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})"
                );
            }
        }

        // set access list if specified
        if let Some(access_list) = match self.tx.access_list {
            None => None,
            Some(None) => Some(provider.create_access_list(&deployer.tx).await?.access_list),
            Some(Some(ref access_list)) => Some(access_list.clone()),
        } {
            deployer.tx.set_access_list(access_list);
        }

        if self.tx.gas_limit.is_none() {
            let request = if browser_signer.is_some() && chain.is_tempo() {
                deployer.tx.browser_wallet_gas_estimation_request()
            } else {
                deployer.tx.clone()
            };
            let estimated = provider.estimate_gas(request).await?;
            deployer.tx.set_gas_limit(estimated);
        }

        // Before we actually deploy the contract we try check if the verify settings are valid
        let mut constructor_args = None;
        if self.verify {
            if !args.is_empty() {
                let encoded_args = abi
                    .constructor()
                    .ok_or_else(|| eyre::eyre!("could not find constructor"))?
                    .abi_encode_input(&args)?;
                constructor_args = Some(hex::encode(encoded_args));
            }

            self.verify_preflight_check(constructor_args.clone(), &id).await?;
        }

        if dry_run {
            if shell::is_json() {
                let output = json!({
                    "contract": self.contract.name,
                    "transaction": &deployer.tx,
                    "abi":&abi
                });
                sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
            } else {
                sh_warn!("Dry run enabled, not broadcasting transaction\n")?;

                sh_println!("Contract: {}", self.contract.name)?;
                sh_println!(
                    "Transaction: {}",
                    serde_json::to_string_pretty(&deployer.tx.clone())?
                )?;
                sh_println!("ABI: {}\n", serde_json::to_string_pretty(&abi)?)?;

                sh_warn!(
                    "To broadcast this transaction, add --broadcast to the previous command. See forge create --help for more."
                )?;
            }

            return Ok(());
        }

        if let Some(ts) = expires_at {
            sh_status!("Transaction expires at unix timestamp {ts}")?;
        }

        let tempo_sponsor = self.tx.tempo.sponsor_config().await?;
        if let Some(sponsor) = &tempo_sponsor {
            sponsor
                .resolve_and_set_fee_token(
                    resolve_unknown_fee_token_symbol.then_some(&provider),
                    Some(chain),
                    &mut deployer.tx,
                )
                .await?;
            sponsor.attach_and_print::<N>(&mut deployer.tx, deployer_address).await?;
        } else {
            let fee_token = resolve_and_set_fee_token(
                resolve_unknown_fee_token_symbol.then_some(&provider),
                Some(chain),
                &mut deployer.tx,
                Some(deployer_address),
            )
            .await?;
            maybe_print_fee_token(resolve_unknown_fee_token_symbol.then_some(&provider), fee_token)
                .await?;
        }

        // Deploy the actual contract
        let (deployed_contract, receipt) = if let Some(browser) = browser_signer {
            // Browser wallet signs and sends the transaction
            let tx_hash = browser.send_transaction_via_browser(deployer.tx).await?;

            // Wait for the transaction to be confirmed, then fetch the receipt.
            provider
                .watch_pending_transaction(alloy_provider::PendingTransactionConfig::new(tx_hash))
                .await?
                .await?;

            let receipt = provider
                .get_transaction_receipt(tx_hash)
                .await?
                .ok_or_else(|| eyre::eyre!("could not get transaction receipt for {tx_hash}"))?;

            if !receipt.status() {
                eyre::bail!("deployment transaction failed (receipt status 0): {tx_hash}");
            }

            let address = receipt
                .contract_address()
                .ok_or_else(|| eyre::eyre!("contract was not deployed"))?;

            (address, receipt)
        } else if let Some(wallet) = tempo_keychain {
            let raw_tx = deployer.tx.sign_with_tempo_wallet(&wallet).await?;

            let receipt = provider
                .send_raw_transaction(&raw_tx)
                .await?
                .with_required_confirmations(1)
                .with_timeout(Some(Duration::from_secs(timeout)))
                .get_receipt()
                .await?;

            let address = receipt
                .contract_address()
                .ok_or_else(|| eyre::eyre!("contract was not deployed"))?;

            (address, receipt)
        } else {
            deployer.send_with_receipt().await?
        };

        let address = deployed_contract;
        let tx_hash = receipt.transaction_hash();
        if shell::is_json() {
            let output = json!({
                "deployer": deployer_address.to_string(),
                "deployedTo": address.to_string(),
                "transactionHash": tx_hash
            });
            sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
        } else {
            sh_println!("Deployer: {deployer_address}")?;
            sh_println!("Deployed to: {address}")?;
            sh_println!("Transaction hash: {tx_hash:?}")?;
        };

        if !self.verify {
            return Ok(());
        }

        sh_status!("Starting contract verification...")?;

        let num_of_optimizations = if let Some(optimizer) = self.build.compiler.optimize {
            optimizer.then(|| self.build.compiler.optimizer_runs.unwrap_or(200))
        } else {
            self.build.compiler.optimizer_runs
        };

        let verify = VerifyArgs {
            address,
            contract: Some(self.contract),
            compiler_version: Some(id.version.to_string()),
            constructor_args,
            constructor_args_path: None,
            no_auto_detect: false,
            use_solc: None,
            num_of_optimizations,
            etherscan: EtherscanOpts { key: self.eth.etherscan.key(), chain: Some(chain) },
            rpc: Default::default(),
            flatten: false,
            force: false,
            skip_is_verified_check: true,
            watch: true,
            print_submission_result_to_stdout: false,
            retry: self.retry,
            libraries: self.build.libraries.clone(),
            root: None,
            verifier: self.verifier,
            via_ir: self.build.compiler.via_ir,
            license_type: self.license_type,
            evm_version: self.build.compiler.evm_version,
            show_standard_json_input: self.show_standard_json_input,
            guess_constructor_args: false,
            compilation_profile: Some(id.profile.clone()),
            language: None,
            creation_transaction_hash: Some(tx_hash),
        };
        // Load the full config (including foundry.toml) so the key used for resolution matches
        // what `verify.run()` will actually use, preventing a "Waiting for sourcify..." message
        // when the run will actually use Etherscan (or vice versa for unknown chains).
        let verify_config = verify.load_config()?;
        let effective_key = verify_config
            .get_etherscan_config_with_chain(Some(chain))?
            .map(|c| c.key)
            .or_else(|| verify_config.etherscan_api_key.clone());
        let resolved_verifier = verify.verifier.resolve(effective_key.as_deref(), Some(chain));
        sh_status!("Waiting for {resolved_verifier} to detect contract deployment...")?;
        verify.run().await
    }
}

impl figment::Provider for CreateArgs {
    fn metadata(&self) -> Metadata {
        Metadata::named("Create Args Provider")
    }

    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
        let mut dict = Dict::default();
        if let Some(timeout) = self.timeout {
            dict.insert("transaction_timeout".to_string(), timeout.into());
        }
        Ok(Map::from([(Config::selected_profile(), dict)]))
    }
}

/// `ContractFactory` is a [`DeploymentTxFactory`] object with an
/// [`Arc`] middleware. This type alias exists to preserve backwards
/// compatibility with less-abstract Contracts.
///
/// For full usage docs, see [`DeploymentTxFactory`].
pub type ContractFactory<N, P> = DeploymentTxFactory<N, P>;

/// Helper which manages the deployment transaction of a smart contract. It
/// wraps a deployment transaction, and retrieves the contract address output
/// by it.
#[derive(Debug)]
#[must_use = "ContractDeploymentTx does nothing unless you `send` it"]
pub struct ContractDeploymentTx<N: Network, P, C> {
    /// the actual deployer, exposed for overriding the defaults
    pub deployer: Deployer<N, P>,
    /// marker for the `Contract` type to create afterwards
    ///
    /// this type will be used to construct it via `From::from(Contract)`
    _contract: PhantomData<C>,
}

impl<N: Network, P: Clone, C> Clone for ContractDeploymentTx<N, P, C> {
    fn clone(&self) -> Self {
        Self { deployer: self.deployer.clone(), _contract: self._contract }
    }
}

impl<N: Network, P, C> From<Deployer<N, P>> for ContractDeploymentTx<N, P, C> {
    fn from(deployer: Deployer<N, P>) -> Self {
        Self { deployer, _contract: PhantomData }
    }
}

/// Helper which manages the deployment transaction of a smart contract
#[derive(Clone, Debug)]
#[must_use = "Deployer does nothing unless you `send` it"]
pub struct Deployer<N: Network, P> {
    /// The deployer's transaction, exposed for overriding the defaults
    pub tx: N::TransactionRequest,
    client: P,
    confs: usize,
    timeout: u64,
}

impl<N: Network, P: Provider<N>> Deployer<N, P> {
    /// Broadcasts the contract deployment transaction and after waiting for it to
    /// be sufficiently confirmed (default: 1), it returns a tuple with the [`Address`] at the
    /// deployed contract's address and the corresponding receipt.
    pub async fn send_with_receipt(
        self,
    ) -> Result<(Address, N::ReceiptResponse), ContractDeploymentError> {
        let receipt = self
            .client
            .borrow()
            .send_transaction(self.tx)
            .await?
            .with_required_confirmations(self.confs as u64)
            .with_timeout(Some(Duration::from_secs(self.timeout)))
            .get_receipt()
            .await?;

        if !receipt.status() {
            return Err(ContractDeploymentError::DeploymentFailed(receipt.transaction_hash()));
        }

        let address =
            receipt.contract_address().ok_or(ContractDeploymentError::ContractNotDeployed)?;

        Ok((address, receipt))
    }
}

/// To deploy a contract to the Ethereum network, a [`ContractFactory`] can be
/// created which manages the Contract bytecode and Application Binary Interface
/// (ABI), usually generated from the Solidity compiler.
#[derive(Clone, Debug)]
pub struct DeploymentTxFactory<N: Network, P> {
    client: P,
    abi: JsonAbi,
    bytecode: Bytes,
    timeout: u64,
    _network: PhantomData<N>,
}

impl<N: Network, P: Provider<N> + Clone> DeploymentTxFactory<N, P> {
    /// Creates a factory for deployment of the Contract with bytecode, and the
    /// constructor defined in the abi. The client will be used to send any deployment
    /// transaction.
    pub const fn new(abi: JsonAbi, bytecode: Bytes, client: P, timeout: u64) -> Self {
        Self { client, abi, bytecode, timeout, _network: PhantomData }
    }

    /// Create a deployment tx using the provided tokens as constructor
    /// arguments
    pub fn deploy_tokens(
        self,
        params: Vec<DynSolValue>,
    ) -> Result<Deployer<N, P>, ContractDeploymentError>
    where
        N::TransactionRequest: FoundryTransactionBuilder<N>,
    {
        // Encode the constructor args & concatenate with the bytecode if necessary
        let data: Bytes = match (self.abi.constructor(), params.is_empty()) {
            (None, false) => return Err(ContractDeploymentError::ConstructorError),
            (None, true) => self.bytecode.clone(),
            (Some(constructor), _) => {
                let input: Bytes = constructor
                    .abi_encode_input(&params)
                    .map_err(ContractDeploymentError::DetokenizationError)?
                    .into();
                // Concatenate the bytecode and abi-encoded constructor call.
                self.bytecode.iter().copied().chain(input).collect()
            }
        };

        // create the tx object. Since we're deploying a contract, `to` is `None`
        let mut tx = N::TransactionRequest::default();
        tx.set_input(data);
        Ok(Deployer { client: self.client.clone(), tx, confs: 1, timeout: self.timeout })
    }
}

#[derive(thiserror::Error, Debug)]
/// An Error which is thrown when interacting with a smart contract
pub enum ContractDeploymentError {
    #[error("constructor is not defined in the ABI")]
    ConstructorError,
    #[error(transparent)]
    DetokenizationError(#[from] alloy_dyn_abi::Error),
    #[error("contract was not deployed")]
    ContractNotDeployed,
    #[error("deployment transaction failed (receipt status 0): {0}")]
    DeploymentFailed(alloy_primitives::TxHash),
    #[error(transparent)]
    RpcError(#[from] TransportError),
}

impl From<PendingTransactionError> for ContractDeploymentError {
    fn from(_err: PendingTransactionError) -> Self {
        Self::ContractNotDeployed
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloy_json_abi::Constructor;
    use alloy_primitives::I256;

    #[test]
    fn can_parse_create() {
        let args: CreateArgs = CreateArgs::parse_from([
            "foundry-cli",
            "src/Domains.sol:Domains",
            "--verify",
            "--retries",
            "10",
            "--delay",
            "30",
            "--license-type",
            "13",
        ]);
        assert_eq!(args.retry.retries, 10);
        assert_eq!(args.retry.delay, 30);
        assert_eq!(args.license_type.as_deref(), Some("13"));
    }

    #[test]
    fn can_parse_create_license_type_spdx() {
        let args: CreateArgs = CreateArgs::parse_from([
            "foundry-cli",
            "src/Domains.sol:Domains",
            "--verify",
            "--license-type",
            "MIT",
        ]);
        assert_eq!(args.license_type.as_deref(), Some("3"));
    }

    #[test]
    fn errors_on_invalid_create_license_type() {
        let err = CreateArgs::try_parse_from([
            "foundry-cli",
            "src/Domains.sol:Domains",
            "--verify",
            "--license-type",
            "definitely-not-a-license",
        ])
        .unwrap_err();
        assert!(err.to_string().contains("unsupported Etherscan license type"));
    }

    #[test]
    fn can_parse_chain_id() {
        let args: CreateArgs = CreateArgs::parse_from([
            "foundry-cli",
            "src/Domains.sol:Domains",
            "--verify",
            "--retries",
            "10",
            "--delay",
            "30",
            "--chain-id",
            "9999",
        ]);
        assert_eq!(args.chain_id().map(|c| c.id()), Some(9999));
    }

    #[test]
    fn test_parse_constructor_args() {
        let args: CreateArgs = CreateArgs::parse_from([
            "foundry-cli",
            "src/Domains.sol:Domains",
            "--constructor-args",
            "Hello",
        ]);
        let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_name","type":"string","internalType":"string"}],"stateMutability":"nonpayable"}"#).unwrap();
        let params = parse_constructor_args(&constructor, &args.constructor_args).unwrap();
        assert_eq!(params, vec![DynSolValue::String("Hello".to_string())]);
    }

    #[test]
    fn test_parse_tuple_constructor_args() {
        let args: CreateArgs = CreateArgs::parse_from([
            "foundry-cli",
            "src/Domains.sol:Domains",
            "--constructor-args",
            "[(1,2), (2,3), (3,4)]",
        ]);
        let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_points","type":"tuple[]","internalType":"struct Point[]","components":[{"name":"x","type":"uint256","internalType":"uint256"},{"name":"y","type":"uint256","internalType":"uint256"}]}],"stateMutability":"nonpayable"}"#).unwrap();
        let _params = parse_constructor_args(&constructor, &args.constructor_args).unwrap();
    }

    #[test]
    fn test_parse_int_constructor_args() {
        let args: CreateArgs = CreateArgs::parse_from([
            "foundry-cli",
            "src/Domains.sol:Domains",
            "--constructor-args",
            "-5",
        ]);
        let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_name","type":"int256","internalType":"int256"}],"stateMutability":"nonpayable"}"#).unwrap();
        let params = parse_constructor_args(&constructor, &args.constructor_args).unwrap();
        assert_eq!(params, vec![DynSolValue::Int(I256::unchecked_from(-5), 256)]);
    }
}
