use std::{collections::HashMap, fmt::Display};
use async_trait::async_trait;
use fuel_core_client::client::pagination::{PaginatedResult, PaginationRequest};
#[doc(no_inline)]
pub use fuel_crypto;
use fuel_crypto::Signature;
use fuel_tx::{Output, Receipt, TxId, TxPointer, UtxoId};
use fuel_types::{AssetId, Bytes32, ContractId, MessageId};
use fuels_core::{
    constants::BASE_ASSET_ID,
    types::{
        bech32::{Bech32Address, Bech32ContractId},
        coin::Coin,
        coin_type::CoinType,
        errors::{Error, Result},
        input::Input,
        message::Message,
        transaction::TxParameters,
        transaction_builders::{ScriptTransactionBuilder, TransactionBuilder},
        transaction_response::TransactionResponse,
    },
};
use provider::ResourceFilter;
use crate::{accounts_utils::extract_message_id, provider::Provider};
mod accounts_utils;
pub mod predicate;
pub mod provider;
pub mod wallet;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait Signer: std::fmt::Debug + Send + Sync {
    type Error: std::error::Error + Send + Sync;
    async fn sign_message<S: Send + Sync + AsRef<[u8]>>(
        &self,
        message: S,
    ) -> std::result::Result<Signature, Self::Error>;
    fn sign_transaction(&self, message: &mut impl TransactionBuilder);
}
#[derive(Debug)]
pub struct AccountError(String);
impl AccountError {
    pub fn no_provider() -> Self {
        Self("No provider was setup: make sure to set_provider in your account!".to_string())
    }
}
impl Display for AccountError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{self:?}")
    }
}
impl std::error::Error for AccountError {}
impl From<AccountError> for Error {
    fn from(e: AccountError) -> Self {
        Error::AccountError(e.0)
    }
}
type AccountResult<T> = std::result::Result<T, AccountError>;
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait ViewOnlyAccount: std::fmt::Debug + Send + Sync + Clone {
    fn address(&self) -> &Bech32Address;
    fn try_provider(&self) -> AccountResult<&Provider>;
    async fn get_transactions(
        &self,
        request: PaginationRequest<String>,
    ) -> Result<PaginatedResult<TransactionResponse, String>> {
        Ok(self
            .try_provider()?
            .get_transactions_by_owner(self.address(), request)
            .await?)
    }
    async fn get_coins(&self, asset_id: AssetId) -> Result<Vec<Coin>> {
        Ok(self
            .try_provider()?
            .get_coins(self.address(), asset_id)
            .await?)
    }
    async fn get_asset_balance(&self, asset_id: &AssetId) -> Result<u64> {
        self.try_provider()?
            .get_asset_balance(self.address(), *asset_id)
            .await
            .map_err(Into::into)
    }
    async fn get_messages(&self) -> Result<Vec<Message>> {
        Ok(self.try_provider()?.get_messages(self.address()).await?)
    }
    async fn get_balances(&self) -> Result<HashMap<String, u64>> {
        self.try_provider()?
            .get_balances(self.address())
            .await
            .map_err(Into::into)
    }
    async fn get_spendable_resources(
        &self,
        asset_id: AssetId,
        amount: u64,
    ) -> Result<Vec<CoinType>> {
        let filter = ResourceFilter {
            from: self.address().clone(),
            asset_id,
            amount,
            ..Default::default()
        };
        self.try_provider()?
            .get_spendable_resources(filter)
            .await
            .map_err(Into::into)
    }
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait Account: ViewOnlyAccount {
    async fn get_asset_inputs_for_amount(
        &self,
        asset_id: AssetId,
        amount: u64,
    ) -> Result<Vec<Input>>;
    fn get_asset_outputs_for_amount(
        &self,
        to: &Bech32Address,
        asset_id: AssetId,
        amount: u64,
    ) -> Vec<Output> {
        vec![
            Output::coin(to.into(), amount, asset_id),
            Output::change(self.address().into(), 0, asset_id),
        ]
    }
    async fn add_fee_resources<Tb: TransactionBuilder>(
        &self,
        tb: Tb,
        previous_base_amount: u64,
    ) -> Result<Tb::TxType>;
    async fn transfer(
        &self,
        to: &Bech32Address,
        amount: u64,
        asset_id: AssetId,
        tx_parameters: TxParameters,
    ) -> Result<(TxId, Vec<Receipt>)> {
        let provider = self.try_provider()?;
        let inputs = self.get_asset_inputs_for_amount(asset_id, amount).await?;
        let outputs = self.get_asset_outputs_for_amount(to, asset_id, amount);
        let consensus_parameters = provider.consensus_parameters();
        let tx_builder = ScriptTransactionBuilder::prepare_transfer(inputs, outputs, tx_parameters)
            .with_consensus_parameters(consensus_parameters);
        let previous_base_amount = if asset_id == AssetId::default() {
            amount
        } else {
            0
        };
        let tx = self
            .add_fee_resources(tx_builder, previous_base_amount)
            .await?;
        let tx_id = provider.send_transaction(tx).await?;
        let receipts = provider.get_receipts(&tx_id).await?;
        Ok((tx_id, receipts))
    }
    async fn force_transfer_to_contract(
        &self,
        to: &Bech32ContractId,
        balance: u64,
        asset_id: AssetId,
        tx_parameters: TxParameters,
    ) -> std::result::Result<(String, Vec<Receipt>), Error> {
        let provider = self.try_provider()?;
        let zeroes = Bytes32::zeroed();
        let plain_contract_id: ContractId = to.into();
        let mut inputs = vec![Input::contract(
            UtxoId::new(zeroes, 0),
            zeroes,
            zeroes,
            TxPointer::default(),
            plain_contract_id,
        )];
        inputs.extend(self.get_asset_inputs_for_amount(asset_id, balance).await?);
        let outputs = vec![
            Output::contract(0, zeroes, zeroes),
            Output::change(self.address().into(), 0, asset_id),
        ];
        let params = provider.consensus_parameters();
        let tb = ScriptTransactionBuilder::prepare_contract_transfer(
            plain_contract_id,
            balance,
            asset_id,
            inputs,
            outputs,
            tx_parameters,
        )
        .with_consensus_parameters(params);
        let base_amount = if asset_id == AssetId::default() {
            balance
        } else {
            0
        };
        let tx = self.add_fee_resources(tb, base_amount).await?;
        let tx_id = provider.send_transaction(tx).await?;
        let receipts = provider.get_receipts(&tx_id).await?;
        Ok((tx_id.to_string(), receipts))
    }
    async fn withdraw_to_base_layer(
        &self,
        to: &Bech32Address,
        amount: u64,
        tx_parameters: TxParameters,
    ) -> std::result::Result<(TxId, MessageId, Vec<Receipt>), Error> {
        let provider = self.try_provider()?;
        let inputs = self
            .get_asset_inputs_for_amount(BASE_ASSET_ID, amount)
            .await?;
        let tb = ScriptTransactionBuilder::prepare_message_to_output(
            to.into(),
            amount,
            inputs,
            tx_parameters,
        );
        let tx = self.add_fee_resources(tb, amount).await?;
        let tx_id = provider.send_transaction(tx).await?;
        let receipts = provider.get_receipts(&tx_id).await?;
        let message_id = extract_message_id(&receipts)
            .expect("MessageId could not be retrieved from tx receipts.");
        Ok((tx_id, message_id, receipts))
    }
}
#[cfg(test)]
mod tests {
    use std::str::FromStr;
    use fuel_crypto::{Message, SecretKey};
    use fuel_tx::{Address, Output};
    use fuels_core::types::transaction::Transaction;
    use rand::{rngs::StdRng, RngCore, SeedableRng};
    use super::*;
    use crate::wallet::WalletUnlocked;
    #[tokio::test]
    async fn sign_and_verify() -> std::result::Result<(), Box<dyn std::error::Error>> {
        let mut rng = StdRng::seed_from_u64(2322u64);
        let mut secret_seed = [0u8; 32];
        rng.fill_bytes(&mut secret_seed);
        let secret = secret_seed
            .as_slice()
            .try_into()
            .expect("The seed size is valid");
        let wallet = WalletUnlocked::new_from_private_key(secret, None);
        let message = "my message";
        let signature = wallet.sign_message(message).await?;
        assert_eq!(signature, Signature::from_str("0x8eeb238db1adea4152644f1cd827b552dfa9ab3f4939718bb45ca476d167c6512a656f4d4c7356bfb9561b14448c230c6e7e4bd781df5ee9e5999faa6495163d")?);
        let message = Message::new(message);
        let recovered_address = signature.recover(&message)?;
        assert_eq!(wallet.address().hash(), recovered_address.hash());
        signature.verify(&recovered_address, &message)?;
        Ok(())
    }
    #[tokio::test]
    async fn sign_tx_and_verify() -> std::result::Result<(), Box<dyn std::error::Error>> {
        let secret = SecretKey::from_str(
            "5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1",
        )?;
        let wallet = WalletUnlocked::new_from_private_key(secret, None);
        let mut tb = {
            let input_coin = Input::ResourceSigned {
                resource: CoinType::Coin(Coin {
                    amount: 10000000,
                    owner: wallet.address().clone(),
                    ..Default::default()
                }),
            };
            let output_coin = Output::coin(
                Address::from_str(
                    "0xc7862855b418ba8f58878db434b21053a61a2025209889cc115989e8040ff077",
                )?,
                1,
                Default::default(),
            );
            ScriptTransactionBuilder::prepare_transfer(
                vec![input_coin],
                vec![output_coin],
                Default::default(),
            )
        };
        wallet.sign_transaction(&mut tb); let tx = tb.build()?; let bytes = <[u8; Signature::LEN]>::try_from(tx.witnesses().first().unwrap().as_ref())?;
        let tx_signature = Signature::from_bytes(bytes);
        let message = Message::from_bytes(*tx.id(0.into()));
        let signature = Signature::sign(&wallet.private_key, &message);
        assert_eq!(signature, tx_signature);
        assert_eq!(signature, Signature::from_str("d7027be16db0aada625ac8cd438f9b6187bd74465495ba39511c1ad72b7bb10af4ef582c94cc33433f7a1eb4f2ad21c471473947f5f645e90924ba273e2cee7f")?);
        let recovered_address = signature.recover(&message)?;
        assert_eq!(wallet.address().hash(), recovered_address.hash());
        signature.verify(&recovered_address, &message)?;
        Ok(())
    }
}