use keyring::Sr25519Keyring;
use primitives::AccountId;
use sc_client_api::UsageProvider;
use sp_runtime::OpaqueExtrinsic;

use crate::*;

macro_rules! identify_chain {
    (
        $chain:expr,
        $nonce:ident,
        $current_block:ident,
        $period:ident,
        $genesis:ident,
        $signer:ident,
        $generic_code:expr $(,)*
    ) => {
        match $chain {
            Chain::Ghost => {
                #[cfg(feature = "ghost-native")]
                {
                    use ghost_runtime as runtime;
                    let call = $generic_code;
                    Ok(ghost_sign_call(
                        call,
                        $nonce,
                        $current_block,
                        $period,
                        $genesis,
                        $signer,
                    ))
                }

                #[cfg(not(feature = "ghost-native"))]
                {
                    Err("`ghost-native` feature not enabled")
                }
            }
            Chain::Casper => {
                #[cfg(feature = "casper-native")]
                {
                    use casper_runtime as runtime;
                    let call = $generic_code;
                    Ok(casper_sign_call(
                        call,
                        $nonce,
                        $current_block,
                        $period,
                        $genesis,
                        $signer,
                    ))
                }

                #[cfg(not(feature = "casper-native"))]
                {
                    Err("`casper-native` feature not enabled")
                }
            }
            Chain::Unknown => {
                let _ = $nonce;
                let _ = $current_block;
                let _ = $period;
                let _ = $genesis;
                let _ = $signer;

                Err("Unknown chain")
            }
        }
    };
}

pub struct RemarkBuilder {
    client: Arc<FullClient>,
    chain: Chain,
}

impl RemarkBuilder {
    pub fn new(client: Arc<FullClient>, chain: Chain) -> Self {
        Self { client, chain }
    }
}

impl frame_benchmarking_cli::ExtrinsicBuilder for RemarkBuilder {
    fn pallet(&self) -> &str {
        "system"
    }

    fn extrinsic(&self) -> &str {
        "remark"
    }

    fn build(&self, nonce: u32) -> std::result::Result<OpaqueExtrinsic, &'static str> {
        let period = 128;
        let genesis = self.client.usage_info().chain.best_hash;
        let signer = Sr25519Keyring::Bob.pair();
        let current_block = 0;

        identify_chain! {
            self.chain,
            nonce,
            current_block,
            period,
            genesis,
            signer,
            {
                runtime::RuntimeCall::System(
                    runtime::SystemCall::remark { remark: vec![] }
                )
            },
        }
    }
}

pub struct TransferKeepAliveBuilder {
    client: Arc<FullClient>,
    dest: AccountId,
    chain: Chain,
}

impl TransferKeepAliveBuilder {
    pub fn new(client: Arc<FullClient>, dest: AccountId, chain: Chain) -> Self {
        Self {
            client,
            dest,
            chain,
        }
    }
}

impl frame_benchmarking_cli::ExtrinsicBuilder for TransferKeepAliveBuilder {
    fn pallet(&self) -> &str {
        "balances"
    }

    fn extrinsic(&self) -> &str {
        "transfer_keep_alive"
    }

    fn build(&self, nonce: u32) -> std::result::Result<OpaqueExtrinsic, &'static str> {
        let signer = Sr25519Keyring::Bob.pair();
        let period = 128;
        let genesis = self.client.usage_info().chain.best_hash;
        let current_block = 0;
        let _dest = self.dest.clone();

        identify_chain! {
            self.chain,
            nonce,
            current_block,
            period,
            genesis,
            signer,
            {
                runtime::RuntimeCall::Balances(runtime::BalancesCall::transfer_keep_alive {
                    dest: _dest.into(),
                    value: runtime::ExistentialDeposit::get(),
                })
            },
        }
    }
}

#[cfg(feature = "casper-native")]
fn casper_sign_call(
    call: casper_runtime::RuntimeCall,
    nonce: u32,
    current_block: u64,
    period: u64,
    genesis: sp_core::H256,
    acc: sp_core::sr25519::Pair,
) -> OpaqueExtrinsic {
    use codec::Encode;
    use sp_core::Pair;

    let extra: casper_runtime::SignedExtra = (
        frame_system::CheckNonZeroSender::<casper_runtime::Runtime>::new(),
        frame_system::CheckSpecVersion::<casper_runtime::Runtime>::new(),
        frame_system::CheckTxVersion::<casper_runtime::Runtime>::new(),
        frame_system::CheckGenesis::<casper_runtime::Runtime>::new(),
        frame_system::CheckMortality::<casper_runtime::Runtime>::from(
            sp_runtime::generic::Era::mortal(period, current_block),
        ),
        frame_system::CheckNonce::<casper_runtime::Runtime>::from(nonce),
        frame_system::CheckWeight::<casper_runtime::Runtime>::new(),
        pallet_transaction_payment::ChargeTransactionPayment::<casper_runtime::Runtime>::from(0),
    );

    let payload = casper_runtime::SignedPayload::from_raw(
        call.clone(),
        extra.clone(),
        (
            (),
            casper_runtime::VERSION.spec_version,
            casper_runtime::VERSION.transaction_version,
            genesis,
            genesis,
            (),
            (),
            (),
        ),
    );

    let signature = payload.using_encoded(|p| acc.sign(p));
    casper_runtime::UncheckedExtrinsic::new_signed(
        call,
        sp_runtime::AccountId32::from(acc.public()).into(),
        primitives::Signature::Sr25519(signature),
        extra,
    )
    .into()
}

pub fn benchmark_inherent_data() -> std::result::Result<inherents::InherentData, inherents::Error> {
    use inherents::InherentDataProvider;
    let mut inherent_data = inherents::InherentData::new();

    let d = std::time::Duration::from_millis(0);
    let timestamp = sp_timestamp::InherentDataProvider::new(d.into());
    futures::executor::block_on(timestamp.provide_inherent_data(&mut inherent_data))?;

    Ok(inherent_data)
}
