use crate::{
    order_book::DEFAULT_METHOD_GAS,
    utxo_manager::{
        FuelTxCoin,
        SharedUtxoManager,
    },
    wallet_ext::{
        BuilderData,
        SendResult,
        WalletExt,
    },
};
use fuel_core_client::client::{
    FuelClient,
    types::TransactionStatus,
};
use fuel_core_types::{
    blockchain::transaction::TransactionExt,
    fuel_tx::{
        Chargeable,
        Finalizable,
        Input,
        Output,
        Receipt,
        Script,
        Transaction,
        TxId,
        TxPointer,
        UniqueIdentifier,
    },
    fuel_types::ContractId,
    services::executor::TransactionExecutionResult,
};
use fuels::{
    accounts::ViewOnlyAccount,
    core::traits::{
        Parameterize,
        Tokenizable,
    },
    prelude::{
        CallHandler,
        Wallet,
    },
    programs::{
        calls::{
            traits::{
                ContractDependencyConfigurator,
                ResponseParser,
                TransactionTuner,
            },
            utils::find_ids_of_missing_contracts,
        },
        responses::CallResponse,
    },
    types::{
        BlockHeight,
        errors::{
            Error as FuelsError,
            Result as FuelsResult,
        },
        transaction_builders::VariableOutputPolicy,
        tx_status::TxStatus,
    },
};
use std::{
    collections::HashSet,
    fmt::Debug,
    future::Future,
};

pub trait CallHandlerExt<T> {
    /// Assemble, dry-run-estimate (if configured), submit, and await the
    /// status of a contract call.
    ///
    /// `dry_run_client`, when `Some`, is used for gas-estimation dry-runs;
    /// `submit_clients`, when non-empty, is fanned out for the actual
    /// submission — the first client to return a valid status wins. When
    /// `dry_run_client` is `None` or `submit_clients` is empty, the
    /// account's provider client is used as a fallback. This lets callers
    /// route dry-runs to a general-purpose sentry and submissions to one
    /// or more dedicated nodes.
    fn almost_sync_call(
        self,
        builder_date: &BuilderData,
        utxo_manager: &SharedUtxoManager,
        tx_config: &Option<TransactionConfig>,
        dry_run_client: Option<&FuelClient>,
        submit_clients: &[FuelClient],
    ) -> impl Future<Output = FuelsResult<SendResult<FuelsResult<CallResponse<T>>>>>;
}

#[derive(Debug, Clone, Copy, Default)]
pub struct TransactionConfig {
    pub min_gas_limit: u64,
    pub estimate_gas_usage: bool,
    pub expiration_height: Option<BlockHeight>,
}

impl TransactionConfig {
    pub fn builder() -> TransactionConfigBuilder {
        TransactionConfigBuilder::new()
    }
}

#[derive(Debug, Clone, Copy, Default)]
pub struct TransactionConfigBuilder {
    min_gas_limit: Option<u64>,
    estimate_gas_usage: Option<bool>,
    expiration_height: Option<BlockHeight>,
}

impl TransactionConfigBuilder {
    pub fn new() -> Self {
        TransactionConfigBuilder {
            min_gas_limit: None,
            estimate_gas_usage: None,
            expiration_height: None,
        }
    }

    pub fn with_min_gas_limit(&mut self, min_gas_limit: u64) -> &mut Self {
        self.min_gas_limit = Some(min_gas_limit);
        self
    }

    pub fn with_estimate_gas_usage(&mut self, estimate_gas_usage: bool) -> &mut Self {
        self.estimate_gas_usage = Some(estimate_gas_usage);
        self
    }

    pub fn min_gas_limit(&self) -> Option<u64> {
        self.min_gas_limit
    }

    pub fn estimate_gas_usage(&self) -> Option<bool> {
        self.estimate_gas_usage
    }

    pub fn with_expiration_height(
        &mut self,
        expiration_height: BlockHeight,
    ) -> &mut Self {
        self.expiration_height = Some(expiration_height);
        self
    }

    pub fn expiration_height(&self) -> Option<BlockHeight> {
        self.expiration_height
    }

    pub fn build(self) -> TransactionConfig {
        TransactionConfig {
            min_gas_limit: self.min_gas_limit.unwrap_or(DEFAULT_METHOD_GAS),
            estimate_gas_usage: self.estimate_gas_usage.unwrap_or(true),
            expiration_height: self.expiration_height,
        }
    }
}

impl<C, T> CallHandlerExt<T> for CallHandler<Wallet, C, T>
where
    C: ContractDependencyConfigurator + TransactionTuner + ResponseParser,
    T: Tokenizable + Parameterize + Debug,
{
    #[tracing::instrument(skip_all)]
    async fn almost_sync_call(
        self,
        builder_date: &BuilderData,
        utxo_manager: &SharedUtxoManager,
        tx_config: &Option<TransactionConfig>,
        dry_run_client: Option<&FuelClient>,
        submit_clients: &[FuelClient],
    ) -> FuelsResult<SendResult<FuelsResult<CallResponse<T>>>> {
        let tx_config = tx_config.unwrap_or_default();
        let consensus_parameters = &builder_date.consensus_parameters;
        let tb =
            self.transaction_builder_with_parameters(consensus_parameters, vec![])?;

        let owner = self.account.address();
        let secret_key = self.account.signer().secret_key();
        let base_asset_id = *consensus_parameters.base_asset_id();

        let max_fee = builder_date.max_fee();

        let input_coins = {
            let mut utxo_manager = utxo_manager.lock().await;
            utxo_manager
                .guaranteed_extract_coins(owner, base_asset_id, max_fee as u128)
                .map_err(|e| FuelsError::Other(e.to_string()))
        }?;
        let coins_iter = input_coins.iter();
        let account = self.account.clone();

        let assemble_tx = async move {
            let witness_limit = crate::wallet_ext::SIGNATURE_MARGIN;

            let mut builder =
                fuel_core_types::fuel_tx::TransactionBuilder::<Script>::script(
                    tb.script,
                    tb.script_data,
                );
            builder
                .with_chain_id(consensus_parameters.chain_id())
                .max_fee_limit(max_fee)
                .witness_limit(witness_limit as u64);

            if let Some(expiration_height) = tx_config.expiration_height {
                builder.expiration(expiration_height);
            }

            for coin in coins_iter {
                builder.add_unsigned_coin_input(
                    secret_key,
                    coin.utxo_id,
                    coin.amount,
                    coin.asset_id,
                    TxPointer::default(),
                );
            }

            builder.add_output(Output::Change {
                to: owner,
                amount: 0,
                asset_id: base_asset_id,
            });

            for input in tb.inputs {
                if let fuels::types::input::Input::Contract { contract_id, .. } = input {
                    let contract_index = builder.inputs().len();
                    builder.add_input(Input::contract(
                        Default::default(),
                        Default::default(),
                        Default::default(),
                        Default::default(),
                        contract_id,
                    ));
                    builder.add_output(Output::contract(
                        contract_index as u16,
                        Default::default(),
                        Default::default(),
                    ));
                }
            }

            // Add variable output if policy is Exactly
            if let VariableOutputPolicy::Exactly(variable_outputs) =
                tb.variable_output_policy
            {
                for _ in 0..variable_outputs {
                    builder.add_output(Output::Variable {
                        to: Default::default(),
                        amount: 0,
                        asset_id: Default::default(),
                    });
                }
            }

            let dummy_script = builder.clone().finalize();
            let max_gas = dummy_script.max_gas(
                consensus_parameters.gas_costs(),
                consensus_parameters.fee_params(),
            ) + 1;
            let available_gas =
                consensus_parameters.tx_params().max_gas_per_tx() - max_gas;

            let (missing_contracts, used_gas) = if tx_config.estimate_gas_usage {
                builder.script_gas_limit(available_gas);

                let client =
                    dry_run_client.unwrap_or_else(|| account.provider().client());
                let tx_to_dry_run = builder.clone().finalize().into();

                let result = client
                    .dry_run_opt(
                        &[tx_to_dry_run],
                        Some(false),
                        Some(builder_date.gas_price),
                        None,
                    )
                    .await?
                    .into_iter()
                    .next()
                    .ok_or_else(|| {
                        FuelsError::Other("Dry run failed to return a result".to_string())
                    })?;

                result.result.missing_contracts_and_used_gas()
            } else {
                (Default::default(), 0)
            };

            for contract_id in missing_contracts {
                let contract_index = builder.inputs().len();
                builder.add_input(Input::contract(
                    Default::default(),
                    Default::default(),
                    Default::default(),
                    Default::default(),
                    contract_id,
                ));

                builder.add_output(Output::contract(
                    contract_index as u16,
                    Default::default(),
                    Default::default(),
                ));
            }

            let gas_limit = std::cmp::max(
                tx_config.min_gas_limit,
                std::cmp::min(used_gas * 2 + 100_000, available_gas),
            );
            builder.script_gas_limit(gas_limit);

            Ok(builder.finalize_as_transaction())
        };

        let tx = match assemble_tx.await {
            Ok(tx) => tx,
            Err(e) => {
                // Return coins if tx assembly failed
                let mut utxo_manager = utxo_manager.lock().await;
                utxo_manager.load_from_coins_vec(input_coins);
                return Err(e);
            }
        };

        let tx_id = tx.id(&consensus_parameters.chain_id());

        let chain_id = consensus_parameters.chain_id();
        let send_result = match self
            .account
            .send_transaction(chain_id, &tx, submit_clients)
            .await
        {
            Ok(result) => result,
            Err(e) => {
                let err_msg = e.to_string();
                if crate::wallet_ext::is_coin_invalid_error(&err_msg) {
                    // Coins are invalid (spent, missing, mismatched) — do NOT
                    // return them to the UtxoManager.
                    tracing::warn!(
                        %tx_id,
                        "Transaction rejected due to invalid coins, \
                         not returning coins to UtxoManager: {err_msg}",
                    );
                } else {
                    // Non-coin error (network, gas, etc.) — coins are still
                    // valid, return them.
                    tracing::warn!(
                        %tx_id,
                        "Transaction failed, returning coins to UtxoManager: {err_msg}",
                    );
                    let mut utxo_manager = utxo_manager.lock().await;
                    utxo_manager.load_from_coins_vec(input_coins);
                }
                return Err(FuelsError::Other(format!(
                    "Failed to send transaction {tx_id}: {e}"
                )));
            }
        };

        // Transaction was accepted — spawn background task to return coins
        // if the transaction expires without being confirmed.
        maybe_return_coins(
            &self.account,
            &tx,
            tx_id,
            tx_config.expiration_height,
            utxo_manager,
        );

        {
            let mut utxo_manager = utxo_manager.lock().await;
            utxo_manager.load_from_coins_vec(send_result.known_coins.clone());
            utxo_manager.load_from_coins_vec(send_result.dynamic_coins.clone());
        }

        let failure_logs = match &send_result.tx_status {
            TxStatus::Success(_)
            | TxStatus::PreconfirmationSuccess(_)
            | TxStatus::Submitted
            | TxStatus::SqueezedOut(_) => None,
            TxStatus::Failure(failure) | TxStatus::PreconfirmationFailure(failure) => {
                let result = self.log_decoder.decode_logs(&failure.receipts);
                tracing::error!(tx_id = %&send_result.tx_id, "Failed to process transaction: {result:?}");
                Some(result)
            }
        };

        let tx_status =
            self.get_response(send_result.tx_status)
                .map_err(|e: FuelsError| {
                    if let Some(failure_logs) = &failure_logs {
                        FuelsError::Other(format!(
                            "Transaction {tx_id} failed with logs: {failure_logs:?} and error: {e}"
                        ))
                    } else {
                        FuelsError::Other(format!(
                            "Failed to get transaction status {tx_id}: {e}"
                        ))
                    }
                });

        let result = SendResult {
            tx_id: send_result.tx_id,
            tx_status,
            known_coins: send_result.known_coins,
            dynamic_coins: send_result.dynamic_coins,
            preconf_rx_time: send_result.preconf_rx_time,
        };

        Ok(result)
    }
}

pub(crate) fn maybe_return_coins(
    account: &Wallet,
    tx: &Transaction,
    tx_id: TxId,
    expiration_height: Option<BlockHeight>,
    utxo_manager: &SharedUtxoManager,
) {
    if let Some(expiration_height) = expiration_height {
        let tx_inputs = tx.inputs().into_owned();
        let provider = account.provider().clone();
        let utxo_manager = utxo_manager.clone();

        tokio::spawn(async move {
            // Wait until we reach expiration_block_height + 1
            let target_height = expiration_height.succ().expect("shouldn't happen; qed");

            // The client should be unique to avoid required height for regular use
            let mut client = FuelClient::new(provider.url())
                .expect("The URL is correct because we send transactions before; qed");
            match client
                .with_required_fuel_block_height(Some(target_height))
                .transaction(&tx_id)
                .await
            {
                Ok(Some(tx_response)) => {
                    // Transaction exists, check its status
                    let status = tx_response.status;
                    match status {
                        TransactionStatus::Success { .. }
                        | TransactionStatus::Failure { .. } => {
                            // Transaction is confirmed or failed, don't return coins
                            tracing::debug!(
                                %tx_id,
                                "Transaction is confirmed/failed at height {}",
                                target_height
                            );
                        }
                        _ => {
                            // Transaction exists but not confirmed/failed, return coins
                            tracing::warn!(
                                %tx_id,
                                "Transaction not confirmed/failed at height {target_height:?}, returning coins",
                            );
                            let coins = tx_inputs
                                .iter()
                                .filter_map(|input| FuelTxCoin::try_from(input).ok());
                            let mut utxo_manager = utxo_manager.lock().await;
                            utxo_manager.load_from_coins_vec(coins.collect());
                        }
                    }
                }
                Ok(None) => {
                    // Transaction doesn't exist in fuel-core — coins may have
                    // been spent or invalidated, so do NOT return them.
                    tracing::warn!(
                        %tx_id,
                        "Transaction not found at height {target_height:?}, \
                         not returning coins (may be invalid)",
                    );
                }
                Err(err) => {
                    tracing::error!(
                        %tx_id,
                        "Failed to get transaction status: {err:?} to return coins",
                    );
                }
            }
        });
    }
}

pub(crate) trait TransactionStatusExt {
    fn missing_contracts_and_used_gas(&self) -> (HashSet<ContractId>, u64);
}

impl TransactionStatusExt for TransactionExecutionResult {
    fn missing_contracts_and_used_gas(&self) -> (HashSet<ContractId>, u64) {
        let contracts = find_ids_of_missing_contracts(self.receipts());
        let used_gas = self
            .receipts()
            .iter()
            .rfind(|r| matches!(r, Receipt::ScriptResult { .. }))
            .map(|script_result| {
                script_result
                    .gas_used()
                    .expect("could not retrieve gas used from ScriptResult")
            })
            .unwrap_or(0);

        (contracts.into_iter().collect(), used_gas)
    }
}
