use crate::{
    backend::{
        sube::{initialize_client, SubeClient},
        wallet::{get_account, get_address, get_wallet, sign},
    },
    model::{
        error::{Cause, Error},
        result::Result,
    },
};

use parity_scale_codec::{Compact, Encode};
use sp_core::{crypto::Ss58Codec, hexdisplay::AsBytesRef};
use sube::{
    meta::{Meta, Pallet},
    Backend,
};

/// Builds a [transfer extrinsic](https://github.com/paritytech/subxt/blob/8484c18624783af36476fc5bf6a0f08d5363a3db/subxt/src/tx/tx_client.rs#L124-L207), then
/// sends it using sube
pub async fn transfer(uname: String, dest: String, value: u128) -> Result<()> {
    let sube = initialize_client("wss://westend-rpc.polkadot.io")
        .await
        .map_err(|e| Error::Sube(Box::new(e)))?;

    let call_data = construct_transfer_call(&sube, &dest, value).await?;
    let wallet = get_wallet(&uname).await?;
    let account = get_account(&wallet).await?;
    let from_address = format!("0x{}", &account);

    let (extra_params, signature_payload) =
        construct_extrinsic_data(&sube, &from_address, &call_data).await?;

    let signature = sign(&wallet, signature_payload).await?;

    let extrinsic_call = {
        let encoded_inner = [
            // header: "is signed" (1 byte) + transaction protocol version (7 bytes)
            vec![0b10000000 + 4u8],
            // signer
            vec![0x00],
            account.public().as_ref().to_vec(),
            // signature
            signature,
            // extra
            extra_params,
            // call data
            call_data,
        ]
        .concat();

        let len = Compact(
            u32::try_from(encoded_inner.len()).expect("extrinsic size expected to be <4GB"),
        )
        .encode();

        [len, encoded_inner].concat()
    };

    println!("Ready to go: 0x{}", hex::encode(&extrinsic_call));

    sube.submit(extrinsic_call).await.map_err(move |e| {
        dbg!(&e);
        Error::Sube(Box::new(e))
    })?;

    Ok(())
}

async fn construct_transfer_call(sube: &SubeClient, dest: &str, value: u128) -> Result<Vec<u8>> {
    let meta = sube
        .metadata()
        .await
        .map_err(move |e| Error::Sube(Box::new(e)))?;

    let (call_ty, pallet_index) = {
        let pallet = meta
            .pallet_by_name("Balances")
            .expect("pallet does not exist");
        (
            pallet
                .get_calls()
                .expect("pallet does not have calls")
                .ty
                .id(),
            pallet.index,
        )
    };

    let dest = get_address(&dest).await?;

    let mut encoded_call = vec![pallet_index];
    let call_payload = serde_json::json!({
        "transfer": {
            "dest": {
                "Id": dest.as_bytes_ref(),
            },
            "value": value as u64,
        }
    });

    let call_data = sube.encode(call_payload, call_ty).await.map_err(|e| {
        dbg!(&e);
        Error::Sube(Box::new(e))
    })?;

    encoded_call.extend(call_data);

    println!(
        "
Transferring {} to {} (0x{})
    Hex-encoded call: {}\n",
        value,
        &dest.to_ss58check(),
        hex::encode(&dest),
        format!("0x{}", hex::encode(&encoded_call)),
    );

    Ok(encoded_call)
}

async fn construct_extrinsic_data(
    sube: &SubeClient,
    from_address: &str,
    call_data: &Vec<u8>,
) -> Result<(Vec<u8>, Vec<u8>)> {
    let extra_params = {
        // ImmortalEra
        let era = 0u8;

        // Impl. Note: in a real-world use case, you should store your account's nonce somewhere else
        let account_info = sube
            .query(&format!("system/account/{}", &from_address))
            .await
            .map_err(move |e| Error::Sube(Box::new(e)))?;
        let account_info =
            <serde_json::Value as std::str::FromStr>::from_str(&account_info.to_string())
                .map_err(move |e| Error::Codec(Box::new(e)))?;
        let nonce = account_info
            .get("nonce")
            .unwrap_or(&serde_json::json!(0))
            .as_u64()
            .expect("nonce be a number");

        let tip: u128 = 0;

        [vec![era], Compact(nonce).encode(), Compact(tip).encode()].concat()
    };

    let additional_params = {
        // Error: Still failing to deserialize the const
        let metadata = sube
            .metadata()
            .await
            .map_err(|e| Error::Sube(Box::new(e)))?
            .clone();

        let mut constants = metadata
            .pallet_by_name("System")
            .ok_or(Error::Codec(Box::new(Cause::from(
                "System pallet not found on metadata",
            ))))?
            .constants
            .clone()
            .into_iter();

        let data = constants
            .find(|c| c.name == "Version")
            .ok_or(Error::Codec(Box::new(Cause::from(
                "System_Version constant not found",
            ))))?
            .clone()
            .to_owned();

        let chain_version = sube
            .decode(data.value.to_vec(), data.ty.id())
            .await
            .map_err(|e| Error::Codec(Box::new(e)))?;
        let chain_version =
            serde_json::to_value(chain_version).map_err(|e| Error::Codec(Box::new(e)))?;

        let spec_version = chain_version
            .get("spec_version")
            .ok_or(Error::Codec(Box::new(Cause::from("spec_version not found"))))?
            .as_u64()
            .ok_or(Error::Codec(Box::new(Cause::from("spec_version not a Number"))))? as u32;
        let transaction_version = chain_version
            .get("transaction_version")
            .ok_or(Error::Codec(Box::new(Cause::from("transaction_version not found"))))?
            .as_u64()
            .ok_or(Error::Codec(Box::new(Cause::from("transaction_version not a Number"))))? as u32;
        let genesis_block: Vec<u8> = sube
            .block_info(Some(0u32))
            .await
            .map_err(move |e| Error::Sube(Box::new(e)))?
            .into();

        [
            spec_version.to_le_bytes().to_vec(),
            transaction_version.to_le_bytes().to_vec(),
            genesis_block.clone(),
            genesis_block.clone(),
        ]
        .concat()
    };

    let signature_payload = [
        call_data.clone(),
        extra_params.clone(),
        additional_params.clone(),
    ]
    .concat();

    Ok((extra_params, signature_payload))
}